Merge branch 'staging' into feat/stainless
This commit is contained in:
@@ -1,33 +1,36 @@
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { APIVersion, AppEnv, Customer, Organization } from "@autumn/shared";
|
||||
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
|
||||
import {
|
||||
APIVersion,
|
||||
type AppEnv,
|
||||
type Customer,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import { addHours, addMonths } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { rewards } from "tests/global.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import {
|
||||
advanceTestClock,
|
||||
completeCheckoutForm,
|
||||
getDiscount,
|
||||
} from "tests/utils/stripeUtils.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import {
|
||||
addPrefixToProducts,
|
||||
getBasePrice,
|
||||
} from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
|
||||
const testCase = "coupon1";
|
||||
|
||||
@@ -65,7 +68,7 @@ const simulateOneCycle = async ({
|
||||
});
|
||||
|
||||
// Expected invoice total
|
||||
let expectedTotal = await getExpectedInvoiceTotal({
|
||||
const expectedTotal = await getExpectedInvoiceTotal({
|
||||
usage: [{ featureId: TestFeature.Words, value: usage }],
|
||||
customerId,
|
||||
productId: pro.id,
|
||||
@@ -117,7 +120,7 @@ describe(
|
||||
`${testCase} - Testing invoice credits reward, apply to all product`,
|
||||
),
|
||||
() => {
|
||||
let customerId = "coupon1";
|
||||
const customerId = "coupon1";
|
||||
let stripeCli: Stripe;
|
||||
let customer: Customer;
|
||||
let testClockId: string;
|
||||
@@ -125,7 +128,7 @@ describe(
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
|
||||
let couponAmount = rewards.rolloverAll.discount_config.discount_value;
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
import {
|
||||
advanceClockForInvoice,
|
||||
completeCheckoutForm,
|
||||
getDiscount,
|
||||
} from "tests/utils/stripeUtils.js";
|
||||
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { expect } from "chai";
|
||||
import {
|
||||
APIVersion,
|
||||
AppEnv,
|
||||
type AppEnv,
|
||||
CouponDurationType,
|
||||
CreateReward,
|
||||
Organization,
|
||||
type CreateReward,
|
||||
type Organization,
|
||||
RewardType,
|
||||
} from "@autumn/shared";
|
||||
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
|
||||
import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js";
|
||||
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import { addHours, addMonths } from "date-fns";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type Stripe from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { timeout } from "tests/utils/genUtils.js";
|
||||
import { createProducts, createReward } from "tests/utils/productUtils.js";
|
||||
import { completeCheckoutForm, getDiscount } from "tests/utils/stripeUtils.js";
|
||||
import {
|
||||
addPrefixToProducts,
|
||||
getBasePrice,
|
||||
} from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
import { createProducts, createReward } from "tests/utils/productUtils.js";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
|
||||
import { addHours, addMonths } from "date-fns";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
@@ -66,11 +58,11 @@ describe(
|
||||
chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`),
|
||||
() => {
|
||||
let logger: any;
|
||||
let customerId = testCase;
|
||||
const customerId = testCase;
|
||||
let stripeCli: Stripe;
|
||||
let testClockId: string;
|
||||
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let db: DrizzleCli;
|
||||
@@ -163,7 +155,7 @@ describe(
|
||||
value: usage,
|
||||
});
|
||||
|
||||
let usageTotal = await getExpectedInvoiceTotal({
|
||||
const usageTotal = await getExpectedInvoiceTotal({
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
@@ -174,7 +166,7 @@ describe(
|
||||
onlyIncludeUsage: true,
|
||||
});
|
||||
|
||||
let basePrice = getBasePrice({ product: pro });
|
||||
const basePrice = getBasePrice({ product: pro });
|
||||
|
||||
couponAmount = couponAmount - usageTotal;
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { slugify } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Clock, InfoIcon, PlusIcon, XIcon, Zap } from "lucide-react";
|
||||
import { Expression, FeatureUsageType, MeteredConfig } from "@autumn/shared";
|
||||
import { FeatureType } from "@autumn/shared";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
type Expression,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
type MeteredConfig,
|
||||
} from "@autumn/shared";
|
||||
import { PlusIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { notNullish, nullish } from "@/utils/genUtils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { slugify } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { nullish } from "@/utils/genUtils";
|
||||
import { SelectFeatureType } from "./SelectFeatureType";
|
||||
import { SelectFeatureUsageType } from "./SelectFeatureUsageType";
|
||||
|
||||
@@ -70,6 +72,14 @@ export function FeatureConfig({
|
||||
);
|
||||
const [idChanged, setIdChanged] = useState(!!feature.id);
|
||||
|
||||
// Helper function to update meteredConfig and sync to parent
|
||||
const updateMeteredConfig = (newConfig: MeteredConfig) => {
|
||||
setMeteredConfig(newConfig);
|
||||
if (feature.type === FeatureType.Metered) {
|
||||
setFeature({ ...feature, config: newConfig });
|
||||
}
|
||||
};
|
||||
|
||||
const showNameAndId = () => {
|
||||
if (nullish(feature.type)) {
|
||||
return false;
|
||||
@@ -92,7 +102,7 @@ export function FeatureConfig({
|
||||
when purchasing a product
|
||||
</div>
|
||||
<SelectFeatureType feature={feature} setFeature={setFeature} />
|
||||
{feature.type == FeatureType.Metered && (
|
||||
{feature.type === FeatureType.Metered && (
|
||||
<SelectFeatureUsageType feature={feature} setFeature={setFeature} />
|
||||
)}
|
||||
|
||||
@@ -139,7 +149,7 @@ export function FeatureConfig({
|
||||
|
||||
<FilterInput
|
||||
config={meteredConfig}
|
||||
setConfig={setMeteredConfig}
|
||||
setConfig={updateMeteredConfig}
|
||||
eventNameInput={eventNameInput}
|
||||
setEventNameInput={setEventNameInput}
|
||||
setEventNameChanged={setEventNameChanged}
|
||||
@@ -150,6 +160,7 @@ export function FeatureConfig({
|
||||
<a
|
||||
href="https://docs.useautumn.com/features/tracking-usage#using-event-names"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
here.
|
||||
@@ -219,7 +230,7 @@ export const FilterInput = ({
|
||||
const enterClicked = () => {
|
||||
let newFilter: Expression;
|
||||
|
||||
if (filter.value.length == 0) {
|
||||
if (filter.value.length === 0) {
|
||||
newFilter = {
|
||||
property: "",
|
||||
operator: "",
|
||||
@@ -244,12 +255,6 @@ export const FilterInput = ({
|
||||
enableOnFormTags: ["input"],
|
||||
enabled: inputFocused,
|
||||
});
|
||||
|
||||
// useHotkeys(["meta+enter"], enterClicked, {
|
||||
// enableOnContentEditable: true,
|
||||
// enabled: inputFocused,
|
||||
// });
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -266,6 +271,7 @@ export const FilterInput = ({
|
||||
>
|
||||
{value}
|
||||
<button
|
||||
type="button"
|
||||
className="text-zinc-500"
|
||||
onClick={() => onRemoveClicked(index)}
|
||||
>
|
||||
|
||||
@@ -36,9 +36,20 @@ function CreateReward() {
|
||||
(async () => {
|
||||
if (!reward?.id && !reward?.name) {
|
||||
toast.error("ID and name are required");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate product selection for discount rewards
|
||||
if (reward.discount_config) {
|
||||
const { apply_to_all, price_ids } = reward.discount_config;
|
||||
if (!apply_to_all && (!price_ids || price_ids.length === 0)) {
|
||||
toast.error("Please select price(s) to apply this reward to");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await RewardService.createReward({
|
||||
axiosInstance,
|
||||
|
||||
@@ -124,19 +124,6 @@ export const DiscountConfig = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* {config.duration_type !== CouponDurationType.OneOff &&
|
||||
reward.type === RewardType.FixedDiscount && (
|
||||
<div className="w-full ml-1 flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={config.should_rollover}
|
||||
onCheckedChange={(checked) =>
|
||||
setConfig("should_rollover", checked === true)
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-t3">Rollover credits to next invoice</p>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<div className="">
|
||||
{/* <p className="text-t2 mb-2 text-t3">Products</p> */}
|
||||
<FieldLabel>Products</FieldLabel>
|
||||
@@ -179,6 +166,24 @@ const ProductPriceSelector = ({
|
||||
setConfig("price_ids", newPriceIds);
|
||||
};
|
||||
|
||||
// Handle toggling "Apply to all products"
|
||||
const handleApplyToAllToggle = () => {
|
||||
const newApplyToAll = !config.apply_to_all;
|
||||
// If enabling "Apply to all", clear the price_ids
|
||||
if (newApplyToAll) {
|
||||
setReward({
|
||||
...reward,
|
||||
discount_config: {
|
||||
...config,
|
||||
apply_to_all: newApplyToAll,
|
||||
price_ids: [],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setConfig("apply_to_all", newApplyToAll);
|
||||
}
|
||||
};
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
return <p className="text-sm text-t3">No products available</p>;
|
||||
}
|
||||
@@ -192,48 +197,54 @@ const ProductPriceSelector = ({
|
||||
aria-expanded={open}
|
||||
className="w-full min-h-9 flex flex-wrap h-fit py-2 justify-start items-center gap-2 relative hover:bg-zinc-50"
|
||||
>
|
||||
{config.apply_to_all
|
||||
? "All Products"
|
||||
: config.price_ids?.length === 0
|
||||
? "Select Products"
|
||||
: config.price_ids?.map((priceId) => {
|
||||
const item = products
|
||||
.find((p: any) =>
|
||||
p.items.find((i: any) => i.price_id === priceId),
|
||||
)
|
||||
?.items.find((i: any) => i.price_id === priceId);
|
||||
{config.apply_to_all ? (
|
||||
<span className="text-t2">All Products</span>
|
||||
) : config.price_ids?.length === 0 ? (
|
||||
<span className="text-t3">Select products or apply to all</span>
|
||||
) : (
|
||||
config.price_ids?.map((priceId) => {
|
||||
const item = products
|
||||
.find((p: any) =>
|
||||
p.items.find((i: any) => i.price_id === priceId),
|
||||
)
|
||||
?.items.find((i: any) => i.price_id === priceId);
|
||||
|
||||
const text = item
|
||||
? formatProductItemText({
|
||||
item,
|
||||
org,
|
||||
features,
|
||||
})
|
||||
: "Unknown Price";
|
||||
return (
|
||||
<div
|
||||
key={priceId}
|
||||
className="py-1 px-3 text-xs text-t3 border-zinc-300 bg-zinc-100 rounded-full flex items-center gap-2 h-fit max-w-[200px] min-w-0"
|
||||
>
|
||||
<p className="truncate flex-1 min-w-0">{text}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePriceToggle(priceId);
|
||||
}}
|
||||
className="bg-transparent hover:bg-transparent p-0 w-5 h-5"
|
||||
>
|
||||
<X size={12} className="text-t3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
const text = item
|
||||
? formatProductItemText({
|
||||
item,
|
||||
org,
|
||||
features,
|
||||
})
|
||||
: "Unknown Price";
|
||||
return (
|
||||
<div
|
||||
key={priceId}
|
||||
className="py-1 px-3 text-xs text-t3 border-zinc-300 bg-zinc-100 rounded-full flex items-center gap-2 h-fit max-w-[200px] min-w-0"
|
||||
>
|
||||
<p className="truncate flex-1 min-w-0">{text}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePriceToggle(priceId);
|
||||
}}
|
||||
className="bg-transparent hover:bg-transparent p-0 w-5 h-5"
|
||||
>
|
||||
<X size={12} className="text-t3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50 absolute right-2" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search prices..." className="h-9" />
|
||||
<CommandList className="max-h-[300px] overflow-y-auto">
|
||||
@@ -241,9 +252,7 @@ const ProductPriceSelector = ({
|
||||
<CommandEmpty>No prices found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setConfig("apply_to_all", !config.apply_to_all);
|
||||
}}
|
||||
onSelect={handleApplyToAllToggle}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<p>Apply to all products</p>
|
||||
@@ -253,19 +262,27 @@ const ProductPriceSelector = ({
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
{!config.apply_to_all &&
|
||||
products.map((product: any) => (
|
||||
<CommandGroup key={product.id} heading={product.name}>
|
||||
{product.items.length > 0 ? (
|
||||
product.items
|
||||
?.filter((item: ProductItem) => {
|
||||
return !isFeatureItem(item);
|
||||
})
|
||||
.map((item: any) => (
|
||||
products
|
||||
.filter((product: any) => {
|
||||
// Filter out products that have no non-feature items
|
||||
const nonFeatureItems = product.items?.filter(
|
||||
(item: ProductItem) => !isFeatureItem(item),
|
||||
);
|
||||
return nonFeatureItems && nonFeatureItems.length > 0;
|
||||
})
|
||||
.map((product: any) => {
|
||||
const nonFeatureItems = product.items.filter(
|
||||
(item: ProductItem) => !isFeatureItem(item),
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandGroup key={product.id} heading={product.name}>
|
||||
{nonFeatureItems.map((item: any) => (
|
||||
<CommandItem
|
||||
key={item.price_id}
|
||||
value={item.price_id}
|
||||
onSelect={() => handlePriceToggle(item.price_id)}
|
||||
className="cursor-pointer overflow-x-hidden max-w-[380px]"
|
||||
className="cursor-pointer overflow-x-hidden"
|
||||
>
|
||||
<span className="truncate overflow-x-hidden">
|
||||
{formatProductItemText({
|
||||
@@ -279,14 +296,10 @@ const ProductPriceSelector = ({
|
||||
<Check size={12} className="text-t3" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))
|
||||
) : (
|
||||
<CommandItem disabled>
|
||||
<p className="text-sm text-t3">No prices available</p>
|
||||
</CommandItem>
|
||||
)}
|
||||
</CommandGroup>
|
||||
))}
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
@@ -72,6 +72,16 @@ function UpdateReward({
|
||||
const handleUpdate = async () => {
|
||||
setUpdateLoading(true);
|
||||
try {
|
||||
// Validate product selection for discount rewards
|
||||
if (selectedReward.discount_config) {
|
||||
const { apply_to_all, price_ids } = selectedReward.discount_config;
|
||||
if (!apply_to_all && (!price_ids || price_ids.length === 0)) {
|
||||
toast.error("Please select price(s) to apply this reward to");
|
||||
setUpdateLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check migration status and show warning if needed
|
||||
if (products) {
|
||||
const migrationResult = checkRewardMigration(selectedReward, products);
|
||||
|
||||
@@ -11,7 +11,7 @@ export const defaultDiscountConfig: DiscountConfig = {
|
||||
duration_type: CouponDurationType.Months,
|
||||
duration_value: 0,
|
||||
should_rollover: true,
|
||||
apply_to_all: true,
|
||||
apply_to_all: false,
|
||||
price_ids: [],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user