wrote tests for referral programs
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { CreateCoupon } from "@autumn/shared";
|
import { CreateReward } from "@autumn/shared";
|
||||||
|
|
||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import { AxiosInstance } from "axios";
|
import { AxiosInstance } from "axios";
|
||||||
@@ -9,7 +9,7 @@ export class CouponService {
|
|||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
axiosInstance: AxiosInstance;
|
axiosInstance: AxiosInstance;
|
||||||
data: CreateCoupon;
|
data: CreateReward;
|
||||||
}) {
|
}) {
|
||||||
await axiosInstance.post("/v1/coupons", data);
|
await axiosInstance.post("/v1/coupons", data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useState } from "react";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
import { Coupon, Customer } from "@autumn/shared";
|
import { Reward, Customer } from "@autumn/shared";
|
||||||
import { useCustomerContext } from "./CustomerContext";
|
import { useCustomerContext } from "./CustomerContext";
|
||||||
import { CusService } from "@/services/customers/CusService";
|
import { CusService } from "@/services/customers/CusService";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -76,7 +76,7 @@ export const CustomerToolbar = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex text-sm items-center justify-between w-full gap-2">
|
<div className="flex text-sm items-center justify-between w-full gap-2">
|
||||||
<p className="text-t2">Add Coupon</p>
|
<p className="text-t2">Add Reward</p>
|
||||||
<FontAwesomeIcon icon={faTicket} size="sm" />
|
<FontAwesomeIcon icon={faTicket} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SelectContent } from "@/components/ui/select";
|
|||||||
import { SelectValue } from "@/components/ui/select";
|
import { SelectValue } from "@/components/ui/select";
|
||||||
import { SelectTrigger } from "@/components/ui/select";
|
import { SelectTrigger } from "@/components/ui/select";
|
||||||
import { DialogFooter } from "@/components/ui/dialog";
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Coupon } from "@autumn/shared";
|
import { Reward } from "@autumn/shared";
|
||||||
import { Select, SelectItem } from "@/components/ui/select";
|
import { Select, SelectItem } from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -22,28 +22,28 @@ const AddCouponDialogContent = ({
|
|||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { coupons } = useCustomerContext();
|
const { coupons } = useCustomerContext();
|
||||||
const [couponSelected, setCouponSelected] = useState<Coupon | null>(null);
|
const [couponSelected, setCouponSelected] = useState<Reward | null>(null);
|
||||||
|
|
||||||
const handleAddClicked = async () => {};
|
const handleAddClicked = async () => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Add Coupon</DialogTitle>
|
<DialogTitle>Add Reward</DialogTitle>
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
value={couponSelected?.internal_id}
|
value={couponSelected?.internal_id}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const coupon = coupons.find((c: Coupon) => c.internal_id === value);
|
const coupon = coupons.find((c: Reward) => c.internal_id === value);
|
||||||
if (coupon) {
|
if (coupon) {
|
||||||
setCouponSelected(coupon);
|
setCouponSelected(coupon);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select Coupon" />
|
<SelectValue placeholder="Select Reward" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{coupons.map((coupon: Coupon) => (
|
{coupons.map((coupon: Reward) => (
|
||||||
<SelectItem key={coupon.internal_id} value={coupon.internal_id}>
|
<SelectItem key={coupon.internal_id} value={coupon.internal_id}>
|
||||||
{coupon.name}
|
{coupon.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -53,7 +53,7 @@ const AddCouponDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="gradientPrimary" onClick={() => handleAddClicked()}>
|
<Button variant="gradientPrimary" onClick={() => handleAddClicked()}>
|
||||||
Add Coupon
|
Add Reward
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faTicketSimple } from "@fortawesome/pro-duotone-svg-icons";
|
import { faTicketSimple } from "@fortawesome/pro-duotone-svg-icons";
|
||||||
import { CouponsTable } from "./coupons/CouponsTable";
|
import { CouponsTable } from "./coupons/CouponsTable";
|
||||||
import CreateCoupon from "./coupons/CreateCoupon";
|
import CreateReward from "./coupons/CreateReward";
|
||||||
|
|
||||||
function ProductsView({ env }: { env: AppEnv }) {
|
function ProductsView({ env }: { env: AppEnv }) {
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||||
@@ -83,7 +83,7 @@ function ProductsView({ env }: { env: AppEnv }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CouponsTable />
|
<CouponsTable />
|
||||||
<CreateCoupon />
|
<CreateReward />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ProductsContext.Provider>
|
</ProductsContext.Provider>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||||
import {
|
import {
|
||||||
CouponDurationType,
|
CouponDurationType,
|
||||||
CreateCoupon,
|
CreateReward,
|
||||||
DiscountType,
|
DiscountType,
|
||||||
Feature,
|
Feature,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
@@ -37,8 +37,8 @@ export const CouponConfig = ({
|
|||||||
coupon,
|
coupon,
|
||||||
setCoupon,
|
setCoupon,
|
||||||
}: {
|
}: {
|
||||||
coupon: CreateCoupon;
|
coupon: CreateReward;
|
||||||
setCoupon: (coupon: CreateCoupon) => void;
|
setCoupon: (coupon: CreateReward) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { org } = useProductsContext();
|
const { org } = useProductsContext();
|
||||||
return (
|
return (
|
||||||
@@ -172,8 +172,8 @@ const ProductPriceSelector = ({
|
|||||||
coupon,
|
coupon,
|
||||||
setCoupon,
|
setCoupon,
|
||||||
}: {
|
}: {
|
||||||
coupon: CreateCoupon;
|
coupon: CreateReward;
|
||||||
setCoupon: (coupon: CreateCoupon) => void;
|
setCoupon: (coupon: CreateReward) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { products, features } = useProductsContext();
|
const { products, features } = useProductsContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import toast from "react-hot-toast";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
import { Coupon, Feature } from "@autumn/shared";
|
import { Reward, Feature } from "@autumn/shared";
|
||||||
import { FeatureService } from "@/services/FeatureService";
|
import { FeatureService } from "@/services/FeatureService";
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
@@ -24,7 +24,7 @@ export const CouponRowToolbar = ({
|
|||||||
coupon,
|
coupon,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
coupon: Coupon;
|
coupon: Reward;
|
||||||
}) => {
|
}) => {
|
||||||
const { env, mutate } = useProductsContext();
|
const { env, mutate } = useProductsContext();
|
||||||
const axiosInstance = useAxiosInstance({ env });
|
const axiosInstance = useAxiosInstance({ env });
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import toast from "react-hot-toast";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
CouponDurationType,
|
CouponDurationType,
|
||||||
CreateCoupon as CreateCouponType,
|
CreateReward as CreateCouponType,
|
||||||
DiscountType,
|
DiscountType,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
@@ -41,7 +41,7 @@ const defaultCoupon: CreateCouponType = {
|
|||||||
apply_to_all: true,
|
apply_to_all: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CreateCoupon() {
|
function CreateReward() {
|
||||||
const { mutate, env } = useProductsContext();
|
const { mutate, env } = useProductsContext();
|
||||||
const axiosInstance = useAxiosInstance({ env: env });
|
const axiosInstance = useAxiosInstance({ env: env });
|
||||||
|
|
||||||
@@ -80,12 +80,12 @@ function CreateCoupon() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
startIcon={<PlusIcon size={15} />}
|
startIcon={<PlusIcon size={15} />}
|
||||||
>
|
>
|
||||||
Create Coupon
|
Create Reward
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="w-[500px]">
|
<DialogContent className="w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Coupon</DialogTitle>
|
<DialogTitle>Create Reward</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* <CreditSystemConfig
|
{/* <CreditSystemConfig
|
||||||
creditSystem={creditSystem}
|
creditSystem={creditSystem}
|
||||||
@@ -106,4 +106,4 @@ function CreateCoupon() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateCoupon;
|
export default CreateReward;
|
||||||
|
|||||||
52
server/src/external/autumn/autumnCli.ts
vendored
52
server/src/external/autumn/autumnCli.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { ErrCode } from "@autumn/shared";
|
import { CreateRewardTrigger, ErrCode } from "@autumn/shared";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
@@ -238,6 +238,56 @@ export class Autumn {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rewards = {
|
||||||
|
create: async (reward: any) => {
|
||||||
|
const data = await this.post(`/rewards`, reward);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
referralPrograms = {
|
||||||
|
create: async (referralProgram: CreateRewardTrigger) => {
|
||||||
|
const data = await this.post(`/reward-triggers`, referralProgram);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
referrals = {
|
||||||
|
createCode: async ({
|
||||||
|
customerId,
|
||||||
|
referralId,
|
||||||
|
}: {
|
||||||
|
customerId: string;
|
||||||
|
referralId: string;
|
||||||
|
}) => {
|
||||||
|
const data = await this.post(`/referrals/code`, {
|
||||||
|
customer_id: customerId,
|
||||||
|
referral_id: referralId,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
redeem: async ({
|
||||||
|
customerId,
|
||||||
|
code,
|
||||||
|
}: {
|
||||||
|
customerId: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
const data = await this.post(`/referrals/redeem`, {
|
||||||
|
customer_id: customerId,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
redemptions = {
|
||||||
|
get: async ({ redemptionId }: { redemptionId: string }) => {
|
||||||
|
const data = await this.get(`/redemptions/${redemptionId}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
initStripe = async () => {
|
initStripe = async () => {
|
||||||
await this.post(`/products/all/init_stripe`, {});
|
await this.post(`/products/all/init_stripe`, {});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import {
|
import {
|
||||||
Coupon,
|
Reward,
|
||||||
CouponDurationType,
|
CouponDurationType,
|
||||||
DiscountType,
|
DiscountType,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { logger } from "@trigger.dev/sdk/v3";
|
import { logger } from "@trigger.dev/sdk/v3";
|
||||||
import { Stripe } from "stripe";
|
import { Stripe } from "stripe";
|
||||||
|
|
||||||
const couponToStripeDuration = (coupon: Coupon) => {
|
const couponToStripeDuration = (coupon: Reward) => {
|
||||||
if (
|
if (
|
||||||
coupon.duration_type === CouponDurationType.OneOff &&
|
coupon.duration_type === CouponDurationType.OneOff &&
|
||||||
coupon.should_rollover
|
coupon.should_rollover
|
||||||
@@ -45,7 +45,7 @@ const couponToStripeValue = ({
|
|||||||
coupon,
|
coupon,
|
||||||
org,
|
org,
|
||||||
}: {
|
}: {
|
||||||
coupon: Coupon;
|
coupon: Reward;
|
||||||
org: Organization;
|
org: Organization;
|
||||||
}) => {
|
}) => {
|
||||||
if (coupon.discount_type === DiscountType.Percentage) {
|
if (coupon.discount_type === DiscountType.Percentage) {
|
||||||
@@ -66,7 +66,7 @@ export const createStripeCoupon = async ({
|
|||||||
org,
|
org,
|
||||||
prices,
|
prices,
|
||||||
}: {
|
}: {
|
||||||
coupon: Coupon;
|
coupon: Reward;
|
||||||
stripeCli: Stripe;
|
stripeCli: Stripe;
|
||||||
org: Organization;
|
org: Organization;
|
||||||
prices: (Price & { product: Product })[];
|
prices: (Price & { product: Product })[];
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const createWebhookEndpoint = async (
|
|||||||
"invoice.created",
|
"invoice.created",
|
||||||
"invoice.finalized",
|
"invoice.finalized",
|
||||||
"subscription_schedule.canceled",
|
"subscription_schedule.canceled",
|
||||||
|
"customer.discount.deleted",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
34
server/src/external/stripe/stripeProductUtils.ts
vendored
34
server/src/external/stripe/stripeProductUtils.ts
vendored
@@ -66,13 +66,35 @@ export const deactivateStripeMeters = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const stripeCli = createStripeCli({ org, env });
|
const stripeCli = createStripeCli({ org, env });
|
||||||
|
|
||||||
const stripeMeters = await stripeCli.billing.meters.list({
|
let allStripeMeters = [];
|
||||||
limit: 100,
|
let hasMore = true;
|
||||||
status: "active",
|
let startingAfter;
|
||||||
});
|
|
||||||
|
|
||||||
for (const meter of stripeMeters.data) {
|
while (hasMore) {
|
||||||
await stripeCli.billing.meters.deactivate(meter.id);
|
const response: any = await stripeCli.billing.meters.list({
|
||||||
|
limit: 100,
|
||||||
|
status: "active",
|
||||||
|
starting_after: startingAfter,
|
||||||
|
});
|
||||||
|
|
||||||
|
allStripeMeters.push(...response.data);
|
||||||
|
hasMore = response.has_more;
|
||||||
|
|
||||||
|
if (hasMore && response.data.length > 0) {
|
||||||
|
startingAfter = response.data[response.data.length - 1].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchSize = 10;
|
||||||
|
for (let i = 0; i < allStripeMeters.length; i += batchSize) {
|
||||||
|
const batch = allStripeMeters.slice(i, i + batchSize);
|
||||||
|
await Promise.all(
|
||||||
|
batch.map((meter) => stripeCli.billing.meters.deactivate(meter.id))
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Deactivated ${i + batch.length}/${allStripeMeters.length} meters`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
server/src/external/stripe/stripeWebhooks.ts
vendored
2
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -167,7 +167,9 @@ stripeWebhookRouter.post(
|
|||||||
discount: event.data.object,
|
discount: event.data.object,
|
||||||
env,
|
env,
|
||||||
logger,
|
logger,
|
||||||
|
res: response,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import stripe, { Stripe } from "stripe";
|
import { Stripe } from "stripe";
|
||||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||||
import { CusProductService } from "@/internal/customers/products/CusProductService.js";
|
import { CusProductService } from "@/internal/customers/products/CusProductService.js";
|
||||||
import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js";
|
import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js";
|
||||||
@@ -24,9 +24,6 @@ import {
|
|||||||
attachToInsertParams,
|
attachToInsertParams,
|
||||||
getPricesForProduct,
|
getPricesForProduct,
|
||||||
} from "@/internal/products/productUtils.js";
|
} from "@/internal/products/productUtils.js";
|
||||||
import { RewardService } from "@/internal/rewards/RewardService.js";
|
|
||||||
import { CouponType, getCouponType } from "@/internal/rewards/rewardUtils.js";
|
|
||||||
import { Decimal } from "decimal.js";
|
|
||||||
import { getStripeExpandedInvoice } from "../stripeInvoiceUtils.js";
|
import { getStripeExpandedInvoice } from "../stripeInvoiceUtils.js";
|
||||||
import { createStripeSub } from "../stripeSubUtils/createStripeSub.js";
|
import { createStripeSub } from "../stripeSubUtils/createStripeSub.js";
|
||||||
import { getAlignedIntervalUnix } from "@/internal/prices/billingIntervalUtils.js";
|
import { getAlignedIntervalUnix } from "@/internal/prices/billingIntervalUtils.js";
|
||||||
|
|||||||
@@ -1,6 +1,79 @@
|
|||||||
import { CusService } from "@/internal/customers/CusService.js";
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
||||||
import { createStripeCli } from "../utils.js";
|
import { createStripeCli } from "../utils.js";
|
||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
import { notNullish, timeout } from "@/utils/genUtils.js";
|
||||||
|
|
||||||
|
export const handleDiscountCompleted = async ({
|
||||||
|
sb,
|
||||||
|
stripeCusId,
|
||||||
|
stripeCli,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
sb: SupabaseClient;
|
||||||
|
stripeCusId: string;
|
||||||
|
stripeCli: Stripe;
|
||||||
|
logger: any;
|
||||||
|
}) => {
|
||||||
|
logger.info(`Checking discount completed`);
|
||||||
|
let customer = await CusService.getByStripeId({
|
||||||
|
sb,
|
||||||
|
stripeId: stripeCusId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!customer) {
|
||||||
|
logger.warn(
|
||||||
|
`Checking discount completed: customer ${stripeCusId} not found`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any redemptions available, and apply to customer if so
|
||||||
|
let redemptions = await RewardRedemptionService.getUnappliedRedemptions({
|
||||||
|
sb,
|
||||||
|
internalCustomerId: customer.internal_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redemptions.length == 0) {
|
||||||
|
logger.info(
|
||||||
|
`Checking discount completed: no redemptions available for customer ${customer.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let redemption = redemptions[0];
|
||||||
|
let reward = redemption.reward_trigger.reward;
|
||||||
|
|
||||||
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
|
stripeCusId
|
||||||
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
|
if (stripeCus && notNullish(stripeCus.discount)) {
|
||||||
|
logger.info(
|
||||||
|
`Checking discount completed: stripe customer ${stripeCusId} already has a discount`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stripeCli.customers.update(stripeCusId, {
|
||||||
|
coupon: reward.internal_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await RewardRedemptionService.update({
|
||||||
|
sb,
|
||||||
|
id: redemption.id,
|
||||||
|
updates: {
|
||||||
|
applied: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Checking discount completed: applied reward ${reward.name} on customer ${customer.name} (${customer.id})`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Redemption ID: ${redemption.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
export async function handleCusDiscountDeleted({
|
export async function handleCusDiscountDeleted({
|
||||||
sb,
|
sb,
|
||||||
@@ -8,12 +81,14 @@ export async function handleCusDiscountDeleted({
|
|||||||
discount,
|
discount,
|
||||||
env,
|
env,
|
||||||
logger,
|
logger,
|
||||||
|
res,
|
||||||
}: {
|
}: {
|
||||||
sb: any;
|
sb: any;
|
||||||
org: any;
|
org: any;
|
||||||
discount: any;
|
discount: any;
|
||||||
env: any;
|
env: any;
|
||||||
logger: any;
|
logger: any;
|
||||||
|
res: any;
|
||||||
}) {
|
}) {
|
||||||
let customer = await CusService.getByStripeId({
|
let customer = await CusService.getByStripeId({
|
||||||
sb,
|
sb,
|
||||||
@@ -52,6 +127,25 @@ export async function handleCusDiscountDeleted({
|
|||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
|
discount.customer
|
||||||
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
|
if (stripeCus && notNullish(stripeCus.discount)) {
|
||||||
|
logger.info(
|
||||||
|
`discount.deleted: stripe customer ${discount.customer} already has a discount`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response first...?
|
||||||
|
res.status(200).send("OK");
|
||||||
|
|
||||||
|
if (notNullish(stripeCus.test_clock)) {
|
||||||
|
// Time out for test clock to complete
|
||||||
|
await timeout(5000);
|
||||||
|
}
|
||||||
|
|
||||||
await stripeCli.customers.update(discount.customer, {
|
await stripeCli.customers.update(discount.customer, {
|
||||||
coupon: reward.internal_id,
|
coupon: reward.internal_id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ const handleInvoicePaidDiscount = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const curCoupon = discount.coupon;
|
const curCoupon = discount.coupon;
|
||||||
|
|
||||||
if (!curCoupon) {
|
if (!curCoupon) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -308,8 +309,6 @@ const handleInvoicePaidDiscount = async ({
|
|||||||
|
|
||||||
console.log(`Updating coupon amount from ${curAmount} to ${newAmount}`);
|
console.log(`Updating coupon amount from ${curAmount} to ${newAmount}`);
|
||||||
|
|
||||||
// Create new coupon with that amount off
|
|
||||||
// console.log("Cur coupon applies to", curCoupon);
|
|
||||||
const newCoupon = await stripeCli.coupons.create({
|
const newCoupon = await stripeCli.coupons.create({
|
||||||
id: `${couponId}_${generateId("roll")}`,
|
id: `${couponId}_${generateId("roll")}`,
|
||||||
name: discount.coupon.name as string,
|
name: discount.coupon.name as string,
|
||||||
|
|||||||
@@ -3,17 +3,6 @@ import { AppEnv, CusProductStatus, Organization } from "@autumn/shared";
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { createStripeCli } from "../utils.js";
|
import { createStripeCli } from "../utils.js";
|
||||||
|
|
||||||
const handleSubPastDue = async ({
|
|
||||||
sb,
|
|
||||||
subscription,
|
|
||||||
}: {
|
|
||||||
sb: any;
|
|
||||||
subscription: any;
|
|
||||||
}) => {
|
|
||||||
// 1. Expire cus products
|
|
||||||
// Cancel subscription entirely
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleSubscriptionUpdated = async ({
|
export const handleSubscriptionUpdated = async ({
|
||||||
sb,
|
sb,
|
||||||
org,
|
org,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { entityRouter } from "./entities/entityRouter.js";
|
|||||||
import { migrationRouter } from "./migrations/migrationRouter.js";
|
import { migrationRouter } from "./migrations/migrationRouter.js";
|
||||||
import rewardRouter from "./rewards/rewardRouter.js";
|
import rewardRouter from "./rewards/rewardRouter.js";
|
||||||
import { rewardTriggerRouter } from "./rewards/rewardTriggerRouter.js";
|
import { rewardTriggerRouter } from "./rewards/rewardTriggerRouter.js";
|
||||||
import { referralRouter } from "./rewards/referralRouter.js";
|
import { redemptionRouter, referralRouter } from "./rewards/referralRouter.js";
|
||||||
|
|
||||||
const apiRouter = Router();
|
const apiRouter = Router();
|
||||||
|
|
||||||
@@ -73,5 +73,6 @@ apiRouter.use("/migrations", migrationRouter);
|
|||||||
// REWARDS
|
// REWARDS
|
||||||
apiRouter.use("/reward-triggers", rewardTriggerRouter);
|
apiRouter.use("/reward-triggers", rewardTriggerRouter);
|
||||||
apiRouter.use("/referrals", referralRouter);
|
apiRouter.use("/referrals", referralRouter);
|
||||||
|
apiRouter.use("/redemptions", redemptionRouter);
|
||||||
|
|
||||||
export { apiRouter };
|
export { apiRouter };
|
||||||
|
|||||||
@@ -191,3 +191,27 @@ referralRouter.post("/redeem", (req, res) =>
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const redemptionRouter = express.Router();
|
||||||
|
|
||||||
|
redemptionRouter.get("/:redemptionId", (req, res) =>
|
||||||
|
routeHandler({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
action: "get redemption by id",
|
||||||
|
handler: async (req: any, res: any) => {
|
||||||
|
const { orgId, env, logtail: logger } = req;
|
||||||
|
const { redemptionId } = req.params;
|
||||||
|
|
||||||
|
let redemption = await RewardRedemptionService.getById({
|
||||||
|
sb: req.sb,
|
||||||
|
id: redemptionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// logger.info("Returning redemption");
|
||||||
|
// logger.info(redemption);
|
||||||
|
|
||||||
|
res.status(200).json(redemption);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { CouponDurationType, CreateCouponSchema } from "@autumn/shared";
|
import { CreateRewardSchema } from "@autumn/shared";
|
||||||
import { handleRequestError } from "@/utils/errorUtils.js";
|
import { handleRequestError } from "@/utils/errorUtils.js";
|
||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
@@ -18,7 +18,7 @@ rewardRouter.post("", async (req: any, res: any) => {
|
|||||||
const { orgId, env } = req;
|
const { orgId, env } = req;
|
||||||
const couponBody = req.body;
|
const couponBody = req.body;
|
||||||
|
|
||||||
const couponData = CreateCouponSchema.parse(couponBody);
|
const couponData = CreateRewardSchema.parse(couponBody);
|
||||||
const org = await OrgService.getFromReq(req);
|
const org = await OrgService.getFromReq(req);
|
||||||
const newCoupon = initCoupon({
|
const newCoupon = initCoupon({
|
||||||
coupon: couponData,
|
coupon: couponData,
|
||||||
@@ -74,12 +74,12 @@ rewardRouter.post("", async (req: any, res: any) => {
|
|||||||
prices,
|
prices,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Coupon successfully created in Stripe");
|
console.log("✅ Reward successfully created in Stripe");
|
||||||
const insertedCoupon = await RewardService.insert({
|
const insertedCoupon = await RewardService.insert({
|
||||||
sb: req.sb,
|
sb: req.sb,
|
||||||
data: newCoupon,
|
data: newCoupon,
|
||||||
});
|
});
|
||||||
console.log("✅ Coupon successfully inserted into db");
|
console.log("✅ Reward successfully inserted into db");
|
||||||
|
|
||||||
res.status(200).json(insertedCoupon);
|
res.status(200).json(insertedCoupon);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -117,7 +117,7 @@ rewardRouter.delete("/:id", async (req: any, res: any) => {
|
|||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Coupon deleted successfully",
|
message: "Reward deleted successfully",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleRequestError({
|
handleRequestError({
|
||||||
@@ -135,7 +135,7 @@ rewardRouter.post("/:id", async (req: any, res: any) => {
|
|||||||
const { orgId, env } = req;
|
const { orgId, env } = req;
|
||||||
const couponBody = req.body;
|
const couponBody = req.body;
|
||||||
|
|
||||||
console.log("Coupon body", couponBody);
|
console.log("Reward body", couponBody);
|
||||||
const org = await OrgService.getFromReq(req);
|
const org = await OrgService.getFromReq(req);
|
||||||
const stripeCli = createStripeCli({
|
const stripeCli = createStripeCli({
|
||||||
org,
|
org,
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ rewardTriggerRouter.post("", (req, res) =>
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("✅ Successfully created reward trigger");
|
||||||
|
console.log(createdRewardTrigger);
|
||||||
|
|
||||||
return res.status(200).json(createdRewardTrigger);
|
return res.status(200).json(createdRewardTrigger);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -281,14 +281,17 @@ export class CusService {
|
|||||||
const { data, error } = await sb
|
const { data, error } = await sb
|
||||||
.from("customers")
|
.from("customers")
|
||||||
.select()
|
.select()
|
||||||
.eq("processor->>id", stripeId)
|
.eq("processor->>id", stripeId);
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
if (data.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
//search customers
|
//search customers
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ productRouter.get("/data", async (req: any, res) => {
|
|||||||
live_pkey: org.live_pkey,
|
live_pkey: org.live_pkey,
|
||||||
default_currency: org.default_currency,
|
default_currency: org.default_currency,
|
||||||
},
|
},
|
||||||
coupons,
|
// coupons,
|
||||||
|
rewards: coupons,
|
||||||
rewardTriggers,
|
rewardTriggers,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ const publicRouterMiddleware = async (req: any, res: any, next: any) => {
|
|||||||
publicRouter.use(publicRouterMiddleware);
|
publicRouter.use(publicRouterMiddleware);
|
||||||
|
|
||||||
publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
|
publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const customerId = req.params.customer_id;
|
const customerId = req.params.customer_id;
|
||||||
console.log("Getting customer (public)", customerId);
|
console.log("Getting customer (public)", customerId);
|
||||||
@@ -103,7 +102,7 @@ publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { main, addOns, balances, invoices } = await getCustomerDetails({
|
const cusData = await getCustomerDetails({
|
||||||
customer,
|
customer,
|
||||||
sb: req.sb,
|
sb: req.sb,
|
||||||
orgId: req.org.id,
|
orgId: req.org.id,
|
||||||
@@ -112,13 +111,7 @@ publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
|
|||||||
logger: req.logtail,
|
logger: req.logtail,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json(cusData);
|
||||||
customer: CustomerResponseSchema.parse(customer),
|
|
||||||
products: main,
|
|
||||||
add_ons: addOns,
|
|
||||||
entitlements: balances,
|
|
||||||
invoices,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleRequestError({ req, error, res, action: "get customer" });
|
handleRequestError({ req, error, res, action: "get customer" });
|
||||||
}
|
}
|
||||||
@@ -130,63 +123,59 @@ publicRouter.get(
|
|||||||
try {
|
try {
|
||||||
const customerId = req.params.customerId;
|
const customerId = req.params.customerId;
|
||||||
|
|
||||||
const customer = await CusService.getById({
|
const customer = await CusService.getById({
|
||||||
sb: req.sb,
|
sb: req.sb,
|
||||||
id: customerId,
|
id: customerId,
|
||||||
orgId: req.org.id,
|
orgId: req.org.id,
|
||||||
env: req.env,
|
env: req.env,
|
||||||
logger: req.logtail,
|
logger: req.logtail,
|
||||||
});
|
|
||||||
|
|
||||||
if (!customer) {
|
|
||||||
return res.status(404).json({
|
|
||||||
message: `Customer ${customerId} not found`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cusProducts = await CusService.getFullCusProducts({
|
|
||||||
sb: req.sb,
|
|
||||||
internalCustomerId: customer.internal_id,
|
|
||||||
inStatuses: [CusProductStatus.Active, CusProductStatus.Scheduled],
|
|
||||||
withProduct: true,
|
|
||||||
withPrices: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!cusProducts || cusProducts.length === 0) {
|
|
||||||
return res.status(200).json({
|
|
||||||
main: [],
|
|
||||||
add_ons: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let main = [];
|
|
||||||
let addOns = [];
|
|
||||||
|
|
||||||
for (const cusProduct of cusProducts) {
|
|
||||||
|
|
||||||
let processed = processFullCusProduct({
|
|
||||||
cusProduct,
|
|
||||||
org: req.org,
|
|
||||||
subs: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (processed.status == CusProductStatus.Trialing) {
|
if (!customer) {
|
||||||
processed.status = CusProductStatus.Active;
|
return res.status(404).json({
|
||||||
|
message: `Customer ${customerId} not found`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let isAddOn = cusProduct.product.is_add_on;
|
const cusProducts = await CusService.getFullCusProducts({
|
||||||
if (isAddOn) {
|
sb: req.sb,
|
||||||
addOns.push(processed);
|
internalCustomerId: customer.internal_id,
|
||||||
} else {
|
inStatuses: [CusProductStatus.Active, CusProductStatus.Scheduled],
|
||||||
main.push(processed);
|
withProduct: true,
|
||||||
|
withPrices: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cusProducts || cusProducts.length === 0) {
|
||||||
|
return res.status(200).json({
|
||||||
|
main: [],
|
||||||
|
add_ons: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
let main = [];
|
||||||
|
let addOns = [];
|
||||||
|
|
||||||
res.status(200).json({
|
for (const cusProduct of cusProducts) {
|
||||||
main,
|
let processed = processFullCusProduct({
|
||||||
|
cusProduct,
|
||||||
|
org: req.org,
|
||||||
|
subs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (processed.status == CusProductStatus.Trialing) {
|
||||||
|
processed.status = CusProductStatus.Active;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAddOn = cusProduct.product.is_add_on;
|
||||||
|
if (isAddOn) {
|
||||||
|
addOns.push(processed);
|
||||||
|
} else {
|
||||||
|
main.push(processed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
main,
|
||||||
add_ons: addOns,
|
add_ons: addOns,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -205,17 +194,17 @@ publicRouter.get(
|
|||||||
orgId: req.org.id,
|
orgId: req.org.id,
|
||||||
env: req.env,
|
env: req.env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const features = await FeatureService.getFeatures({
|
const features = await FeatureService.getFeatures({
|
||||||
sb: req.sb,
|
sb: req.sb,
|
||||||
orgId: req.org.id,
|
orgId: req.org.id,
|
||||||
env: req.env,
|
env: req.env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const prices = product.prices;
|
const prices = product.prices;
|
||||||
|
|
||||||
const options = getOptionsFromPrices(prices, features);
|
const options = getOptionsFromPrices(prices, features);
|
||||||
|
|
||||||
res.status(200).json(options);
|
res.status(200).json(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleRequestError({ req, error, res, action: "get product options" });
|
handleRequestError({ req, error, res, action: "get product options" });
|
||||||
|
|||||||
@@ -2,6 +2,20 @@ import { notNullish } from "@/utils/genUtils.js";
|
|||||||
import { RewardRedemption, RewardTriggerEvent } from "@autumn/shared";
|
import { RewardRedemption, RewardTriggerEvent } from "@autumn/shared";
|
||||||
|
|
||||||
export class RewardRedemptionService {
|
export class RewardRedemptionService {
|
||||||
|
static async getById({ sb, id }: { sb: any; id: string }) {
|
||||||
|
const { data, error } = await sb
|
||||||
|
.from("reward_redemptions")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
static async getByCustomer({
|
static async getByCustomer({
|
||||||
sb,
|
sb,
|
||||||
internalCustomerId,
|
internalCustomerId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateId } from "@/utils/genUtils.js";
|
import { generateId } from "@/utils/genUtils.js";
|
||||||
import { AppEnv, Coupon } from "@autumn/shared";
|
import { AppEnv, Reward } from "@autumn/shared";
|
||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
export class RewardService {
|
export class RewardService {
|
||||||
@@ -8,7 +8,7 @@ export class RewardService {
|
|||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
sb: SupabaseClient;
|
sb: SupabaseClient;
|
||||||
data: Coupon | Coupon[];
|
data: Reward | Reward[];
|
||||||
}) {
|
}) {
|
||||||
const { data: insertedData, error } = await sb
|
const { data: insertedData, error } = await sb
|
||||||
.from("rewards")
|
.from("rewards")
|
||||||
@@ -103,7 +103,7 @@ export class RewardService {
|
|||||||
internalId: string;
|
internalId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
update: Partial<Coupon>;
|
update: Partial<Reward>;
|
||||||
}) {
|
}) {
|
||||||
const { data, error } = await sb
|
const { data, error } = await sb
|
||||||
.from("rewards")
|
.from("rewards")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateId } from "@/utils/genUtils.js";
|
import { generateId } from "@/utils/genUtils.js";
|
||||||
import { Coupon, CreateCoupon } from "@autumn/shared";
|
import { Reward, CreateReward } from "@autumn/shared";
|
||||||
|
|
||||||
export const initCoupon = ({
|
export const initCoupon = ({
|
||||||
coupon,
|
coupon,
|
||||||
@@ -7,7 +7,7 @@ export const initCoupon = ({
|
|||||||
env,
|
env,
|
||||||
id,
|
id,
|
||||||
}: {
|
}: {
|
||||||
coupon: CreateCoupon;
|
coupon: CreateReward;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
env: string;
|
env: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -31,7 +31,7 @@ export enum CouponType {
|
|||||||
Standard = "standard",
|
Standard = "standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCouponType = (coupon: Coupon) => {
|
export const getCouponType = (coupon: Reward) => {
|
||||||
if (!coupon) return null;
|
if (!coupon) return null;
|
||||||
if (coupon.apply_to_all && coupon.should_rollover) {
|
if (coupon.apply_to_all && coupon.should_rollover) {
|
||||||
return CouponType.AddInvoiceBalance;
|
return CouponType.AddInvoiceBalance;
|
||||||
|
|||||||
@@ -43,3 +43,7 @@ export const notNullish = (value: any) => {
|
|||||||
export const formatUnixToDateTime = (unixDate: number) => {
|
export const formatUnixToDateTime = (unixDate: number) => {
|
||||||
return format(new Date(unixDate), "yyyy MMM dd HH:mm:ss");
|
return format(new Date(unixDate), "yyyy MMM dd HH:mm:ss");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const timeout = (ms: number) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts"
|
|||||||
# TEST PARALLEL
|
# TEST PARALLEL
|
||||||
if [ "$1" == "basic-parallel" ]; then
|
if [ "$1" == "basic-parallel" ]; then
|
||||||
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \
|
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \
|
||||||
tests/basic/*.ts \
|
tests/basic/referrals/*.ts \
|
||||||
tests/basic/entities/*.ts \
|
tests/attach/**/*.ts \
|
||||||
# tests/attach/**/*.ts \
|
# tests/basic/*.ts \
|
||||||
|
# tests/basic/entities/*.ts \
|
||||||
|
|
||||||
elif [ "$1" == "advanced-parallel" ]; then
|
elif [ "$1" == "advanced-parallel" ]; then
|
||||||
MOCHA_PARALLEL=true \
|
MOCHA_PARALLEL=true \
|
||||||
$MOCHA_SETUP \
|
$MOCHA_SETUP \
|
||||||
&& $MOCHA_CMD 'tests/advanced/usage/*.ts' \
|
&& $MOCHA_CMD 'tests/advanced/coupons/*.ts' \
|
||||||
# && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\
|
&& $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\
|
||||||
# && $MOCHA_CMD 'tests/advanced/coupons/*.ts' \
|
# && $MOCHA_CMD 'tests/advanced/usage/*.ts' \
|
||||||
|
|
||||||
|
|
||||||
elif [ "$1" == "alex-parallel" ]; then
|
elif [ "$1" == "alex-parallel" ]; then
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import {
|
|||||||
creditSystems,
|
creditSystems,
|
||||||
advanceProducts,
|
advanceProducts,
|
||||||
attachProducts,
|
attachProducts,
|
||||||
coupons,
|
rewards,
|
||||||
oneTimeProducts,
|
oneTimeProducts,
|
||||||
entityProducts,
|
entityProducts,
|
||||||
|
referralPrograms,
|
||||||
} from "./global.js";
|
} from "./global.js";
|
||||||
|
|
||||||
const ORG_SLUG = "unit-test-org";
|
const ORG_SLUG = "unit-test-org";
|
||||||
@@ -32,9 +33,10 @@ describe("Initialize org for tests", () => {
|
|||||||
...oneTimeProducts,
|
...oneTimeProducts,
|
||||||
...entityProducts,
|
...entityProducts,
|
||||||
} as any,
|
} as any,
|
||||||
coupons: { ...coupons } as any,
|
rewards: { ...rewards } as any,
|
||||||
|
rewardTriggers: { ...referralPrograms } as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("--------------------------------");
|
console.log("--------------------------------");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js";
|
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js";
|
||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||||
import { getOriginalCouponId } from "@/internal/coupons/couponUtils.js";
|
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
|
||||||
import { getPriceForOverage } from "@/internal/prices/priceUtils.js";
|
import { getPriceForOverage } from "@/internal/prices/priceUtils.js";
|
||||||
import { Customer } from "@autumn/shared";
|
import { Customer } from "@autumn/shared";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
@@ -8,7 +8,7 @@ import chalk from "chalk";
|
|||||||
import { addDays, addHours, addMonths, format } from "date-fns";
|
import { addDays, addHours, addMonths, format } from "date-fns";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||||
import { coupons, features, products } from "tests/global.js";
|
import { features, products, rewards } from "tests/global.js";
|
||||||
import { compareMainProduct } from "tests/utils/compare.js";
|
import { compareMainProduct } from "tests/utils/compare.js";
|
||||||
import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js";
|
import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js";
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ describe(
|
|||||||
let customer: Customer;
|
let customer: Customer;
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
|
|
||||||
let couponAmount = coupons.rolloverAll.discount_value;
|
let couponAmount = rewards.rolloverAll.discount_value;
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
const { testClockId: testClockId1, customer: customer1 } =
|
const { testClockId: testClockId1, customer: customer1 } =
|
||||||
@@ -65,7 +65,7 @@ describe(
|
|||||||
await completeCheckoutForm(
|
await completeCheckoutForm(
|
||||||
res.checkout_url,
|
res.checkout_url,
|
||||||
undefined,
|
undefined,
|
||||||
coupons.rolloverAll.id
|
rewards.rolloverAll.id
|
||||||
);
|
);
|
||||||
|
|
||||||
await timeout(20000);
|
await timeout(20000);
|
||||||
@@ -88,7 +88,7 @@ describe(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
||||||
coupons.rolloverAll.id
|
rewards.rolloverAll.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Expect amount to be original amount - pro price
|
// Expect amount to be original amount - pro price
|
||||||
@@ -97,7 +97,7 @@ describe(
|
|||||||
console.error("--------------------------------");
|
console.error("--------------------------------");
|
||||||
console.error(
|
console.error(
|
||||||
"Expected stripe cus to have coupon",
|
"Expected stripe cus to have coupon",
|
||||||
coupons.rolloverAll
|
rewards.rolloverAll
|
||||||
);
|
);
|
||||||
console.error("Actual stripe cus discount", cusDiscount);
|
console.error("Actual stripe cus discount", cusDiscount);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -140,13 +140,13 @@ describe(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
||||||
coupons.rolloverAll.id
|
rewards.rolloverAll.id
|
||||||
);
|
);
|
||||||
expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
|
expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("--------------------------------");
|
console.log("--------------------------------");
|
||||||
console.log("coupon1, cycle 1 failed");
|
console.log("coupon1, cycle 1 failed");
|
||||||
console.log("Expected stripe cus to have coupon", coupons.rolloverAll);
|
console.log("Expected stripe cus to have coupon", rewards.rolloverAll);
|
||||||
console.log("Actual stripe cus discount", cusDiscount);
|
console.log("Actual stripe cus discount", cusDiscount);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js";
|
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js";
|
||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||||
import { getOriginalCouponId } from "@/internal/coupons/couponUtils.js";
|
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
|
||||||
import { getPriceForOverage } from "@/internal/prices/priceUtils.js";
|
import { getPriceForOverage } from "@/internal/prices/priceUtils.js";
|
||||||
import { Customer } from "@autumn/shared";
|
import { Customer } from "@autumn/shared";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||||
import { coupons, features, products } from "tests/global.js";
|
import { features, products, rewards } from "tests/global.js";
|
||||||
import { compareMainProduct } from "tests/utils/compare.js";
|
import { compareMainProduct } from "tests/utils/compare.js";
|
||||||
import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js";
|
import { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js";
|
||||||
import {
|
import {
|
||||||
@@ -27,7 +27,7 @@ describe(
|
|||||||
let customer: Customer;
|
let customer: Customer;
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
|
|
||||||
let couponAmount = coupons.rolloverAll.discount_value;
|
let couponAmount = rewards.rolloverUsage.discount_value;
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
const { testClockId: testClockId1, customer: customer1 } =
|
const { testClockId: testClockId1, customer: customer1 } =
|
||||||
@@ -61,7 +61,7 @@ describe(
|
|||||||
await completeCheckoutForm(
|
await completeCheckoutForm(
|
||||||
res.checkout_url,
|
res.checkout_url,
|
||||||
undefined,
|
undefined,
|
||||||
coupons.rolloverUsage.id
|
rewards.rolloverUsage.id
|
||||||
);
|
);
|
||||||
|
|
||||||
await timeout(10000);
|
await timeout(10000);
|
||||||
@@ -85,7 +85,7 @@ describe(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
||||||
coupons.rolloverUsage.id
|
rewards.rolloverUsage.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Expect amount to be original amount - pro price
|
// Expect amount to be original amount - pro price
|
||||||
@@ -94,7 +94,7 @@ describe(
|
|||||||
logger.error("--------------------------------");
|
logger.error("--------------------------------");
|
||||||
logger.error(
|
logger.error(
|
||||||
"Expected stripe cus to have coupon",
|
"Expected stripe cus to have coupon",
|
||||||
coupons.rolloverUsage
|
rewards.rolloverUsage
|
||||||
);
|
);
|
||||||
logger.error("Actual stripe cus discount", cusDiscount);
|
logger.error("Actual stripe cus discount", cusDiscount);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -139,7 +139,7 @@ describe(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
|
||||||
coupons.rolloverUsage.id
|
rewards.rolloverUsage.id
|
||||||
);
|
);
|
||||||
expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
|
expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -147,7 +147,7 @@ describe(
|
|||||||
logger.error("coupon2, cycle 1 failed");
|
logger.error("coupon2, cycle 1 failed");
|
||||||
logger.error(
|
logger.error(
|
||||||
"Expected stripe cus to have coupon",
|
"Expected stripe cus to have coupon",
|
||||||
coupons.rolloverUsage
|
rewards.rolloverUsage
|
||||||
);
|
);
|
||||||
logger.error("Actual stripe cus discount", cusDiscount);
|
logger.error("Actual stripe cus discount", cusDiscount);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { sendGPUEvents } from "../../utils/advancedUsageUtils.js";
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
const PRECISION = 12;
|
const PRECISION = 12;
|
||||||
describe(`${chalk.yellowBright(
|
describe.skip(`${chalk.yellowBright(
|
||||||
"Testing group by -- regular metered1 feature"
|
"Testing group by -- regular metered1 feature"
|
||||||
)}`, () => {
|
)}`, () => {
|
||||||
let customerId = "group-by-basic-metered";
|
let customerId = "group-by-basic-metered";
|
||||||
|
|||||||
194
server/tests/basic/referrals/referrals1.ts
Normal file
194
server/tests/basic/referrals/referrals1.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { products, referralPrograms } from "../../global.js";
|
||||||
|
import { assert } from "chai";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import AutumnError, { Autumn } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
|
import {
|
||||||
|
Customer,
|
||||||
|
ErrCode,
|
||||||
|
ReferralCode,
|
||||||
|
RewardRedemption,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { timeout } from "tests/utils/genUtils.js";
|
||||||
|
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
|
||||||
|
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||||
|
import { addDays, addHours, addMonths } from "date-fns";
|
||||||
|
import { Stripe } from "stripe";
|
||||||
|
import { initCustomer } from "tests/utils/init.js";
|
||||||
|
|
||||||
|
// UNCOMMENT FROM HERE
|
||||||
|
describe(`${chalk.yellowBright(
|
||||||
|
"referrals1: Testing referrals (on checkout)"
|
||||||
|
)}`, () => {
|
||||||
|
let mainCustomerId = "main-referral-1";
|
||||||
|
let redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"];
|
||||||
|
let autumn: Autumn;
|
||||||
|
let stripeCli: Stripe;
|
||||||
|
let testClockId: string;
|
||||||
|
let referralCode: ReferralCode;
|
||||||
|
|
||||||
|
let redemptions: RewardRedemption[] = [];
|
||||||
|
let mainCustomer: Customer;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await setupBefore(this);
|
||||||
|
autumn = this.autumn;
|
||||||
|
stripeCli = this.stripeCli;
|
||||||
|
|
||||||
|
const { testClockId: testClockId1, customer } =
|
||||||
|
await initCustomerWithTestClock({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
sb: this.sb,
|
||||||
|
org: this.org,
|
||||||
|
env: this.env,
|
||||||
|
});
|
||||||
|
testClockId = testClockId1;
|
||||||
|
mainCustomer = customer;
|
||||||
|
|
||||||
|
await autumn.attach({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
productId: products.proWithTrial.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let batchCreate = [];
|
||||||
|
for (let redeemer of redeemers) {
|
||||||
|
batchCreate.push(
|
||||||
|
initCustomer({
|
||||||
|
customerId: redeemer,
|
||||||
|
sb: this.sb,
|
||||||
|
org: this.org,
|
||||||
|
env: this.env,
|
||||||
|
attachPm: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(batchCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create code once", async function () {
|
||||||
|
referralCode = await autumn.referrals.createCode({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
referralId: referralPrograms.onCheckout.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.exists(referralCode.code);
|
||||||
|
|
||||||
|
// Get referral code again
|
||||||
|
let referralCode2 = await autumn.referrals.createCode({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
referralId: referralPrograms.onCheckout.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(referralCode2.code, referralCode.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create redemption for each redeemer and fail if redeemed again", async function () {
|
||||||
|
for (let redeemer of redeemers) {
|
||||||
|
let redemption: RewardRedemption = await autumn.referrals.redeem({
|
||||||
|
customerId: redeemer,
|
||||||
|
code: referralCode.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
redemptions.push(redemption);
|
||||||
|
|
||||||
|
assert.equal(redemption.triggered, false);
|
||||||
|
assert.equal(redemption.applied, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try redeem for redeemer1 again
|
||||||
|
try {
|
||||||
|
let redemption1 = await autumn.referrals.redeem({
|
||||||
|
customerId: redeemers[0],
|
||||||
|
code: referralCode.code,
|
||||||
|
});
|
||||||
|
assert.fail("Should not be able to redeem again");
|
||||||
|
} catch (error) {
|
||||||
|
assert.instanceOf(error, AutumnError);
|
||||||
|
assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be triggered (and applied) when redeemers check out", async function () {
|
||||||
|
for (let i = 0; i < redeemers.length; i++) {
|
||||||
|
let redeemer = redeemers[i];
|
||||||
|
|
||||||
|
await autumn.attach({
|
||||||
|
customerId: redeemer,
|
||||||
|
productId: products.pro.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await timeout(3000);
|
||||||
|
|
||||||
|
// Get redemption object
|
||||||
|
let redemption = await autumn.redemptions.get({
|
||||||
|
redemptionId: redemptions[i].id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if redemption is triggered
|
||||||
|
let count = i + 1;
|
||||||
|
|
||||||
|
if (count > referralPrograms.onCheckout.max_redemptions) {
|
||||||
|
assert.equal(redemption.triggered, false);
|
||||||
|
assert.equal(redemption.applied, false);
|
||||||
|
} else {
|
||||||
|
assert.equal(redemption.triggered, true);
|
||||||
|
assert.equal(redemption.applied, i == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check stripe customer
|
||||||
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
|
mainCustomer.processor?.id
|
||||||
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
|
assert.notEqual(stripeCus.discount, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let curTime = new Date();
|
||||||
|
it("customer should have discount for first purchase", async function () {
|
||||||
|
curTime = addHours(addDays(curTime, 7), 2);
|
||||||
|
await advanceTestClock({
|
||||||
|
testClockId,
|
||||||
|
advanceTo: curTime.getTime(),
|
||||||
|
stripeCli,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Get invoice
|
||||||
|
let { invoices } = await autumn.customers.get(mainCustomerId);
|
||||||
|
|
||||||
|
assert.equal(invoices.length, 2);
|
||||||
|
assert.equal(invoices[0].total, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customer should have discount for second purchase", async function () {
|
||||||
|
// 2. Check that customer has another discount
|
||||||
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
|
mainCustomer.processor?.id
|
||||||
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
|
assert.notEqual(stripeCus.discount, null);
|
||||||
|
|
||||||
|
// 2. Advance test clock to 1 month from start (trigger discount.deleted event)
|
||||||
|
curTime = addHours(addMonths(new Date(), 1), 2);
|
||||||
|
await advanceTestClock({
|
||||||
|
testClockId,
|
||||||
|
advanceTo: curTime.getTime(),
|
||||||
|
stripeCli,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Advance test clock to 1 month + 7 days from start (trigger new invoice)
|
||||||
|
curTime = addDays(curTime, 7);
|
||||||
|
await advanceTestClock({
|
||||||
|
testClockId,
|
||||||
|
advanceTo: curTime.getTime(),
|
||||||
|
stripeCli,
|
||||||
|
});
|
||||||
|
|
||||||
|
// // 3. Get invoice again
|
||||||
|
let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
|
||||||
|
|
||||||
|
assert.equal(invoices2.length, 3);
|
||||||
|
assert.equal(invoices2[0].total, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
171
server/tests/basic/referrals/referrals2.ts
Normal file
171
server/tests/basic/referrals/referrals2.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { products, referralPrograms } from "../../global.js";
|
||||||
|
import { assert } from "chai";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import AutumnError, { Autumn } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
|
import {
|
||||||
|
Customer,
|
||||||
|
ErrCode,
|
||||||
|
ReferralCode,
|
||||||
|
RewardRedemption,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { timeout } from "tests/utils/genUtils.js";
|
||||||
|
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
|
||||||
|
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||||
|
import { addDays, addHours, addMonths } from "date-fns";
|
||||||
|
import { Stripe } from "stripe";
|
||||||
|
import { initCustomer } from "tests/utils/init.js";
|
||||||
|
|
||||||
|
// UNCOMMENT FROM HERE
|
||||||
|
describe(`${chalk.yellowBright(
|
||||||
|
"referrals2: Testing referrals (immediate redemption)"
|
||||||
|
)}`, () => {
|
||||||
|
let mainCustomerId = "main-referral-2";
|
||||||
|
let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"];
|
||||||
|
let autumn: Autumn;
|
||||||
|
let stripeCli: Stripe;
|
||||||
|
let testClockId: string;
|
||||||
|
let referralCode: ReferralCode;
|
||||||
|
|
||||||
|
let redemptions: RewardRedemption[] = [];
|
||||||
|
let mainCustomer: Customer;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await setupBefore(this);
|
||||||
|
autumn = this.autumn;
|
||||||
|
stripeCli = this.stripeCli;
|
||||||
|
|
||||||
|
const { testClockId: testClockId1, customer } =
|
||||||
|
await initCustomerWithTestClock({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
sb: this.sb,
|
||||||
|
org: this.org,
|
||||||
|
env: this.env,
|
||||||
|
});
|
||||||
|
testClockId = testClockId1;
|
||||||
|
mainCustomer = customer;
|
||||||
|
|
||||||
|
let batchCreate = [];
|
||||||
|
for (let redeemer of redeemers) {
|
||||||
|
batchCreate.push(
|
||||||
|
initCustomer({
|
||||||
|
customerId: redeemer,
|
||||||
|
sb: this.sb,
|
||||||
|
org: this.org,
|
||||||
|
env: this.env,
|
||||||
|
attachPm: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(batchCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create code once", async function () {
|
||||||
|
referralCode = await autumn.referrals.createCode({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
referralId: referralPrograms.immediate.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.exists(referralCode.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create redemption for each redeemer and fail if redeemed again", async function () {
|
||||||
|
for (let i = 0; i < redeemers.length; i++) {
|
||||||
|
let redeemer = redeemers[i];
|
||||||
|
let count = i + 1;
|
||||||
|
try {
|
||||||
|
let redemption: RewardRedemption = await autumn.referrals.redeem({
|
||||||
|
customerId: redeemer,
|
||||||
|
code: referralCode.code,
|
||||||
|
});
|
||||||
|
redemptions.push(redemption);
|
||||||
|
|
||||||
|
if (count > referralPrograms.immediate.max_redemptions) {
|
||||||
|
assert.equal(redemption.triggered, false);
|
||||||
|
assert.equal(redemption.applied, false);
|
||||||
|
} else {
|
||||||
|
assert.fail("Should not be able to redeem again");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (count > referralPrograms.immediate.max_redemptions) {
|
||||||
|
assert.instanceOf(error, AutumnError);
|
||||||
|
assert.equal(error.code, ErrCode.ReferralCodeMaxRedemptionsReached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try redeem for redeemer1 again
|
||||||
|
// try {
|
||||||
|
// let redemption1 = await autumn.referrals.redeem({
|
||||||
|
// customerId: redeemers[0],
|
||||||
|
// code: referralCode.code,
|
||||||
|
// });
|
||||||
|
// assert.fail("Should not be able to redeem again");
|
||||||
|
// } catch (error) {
|
||||||
|
// assert.instanceOf(error, AutumnError);
|
||||||
|
// assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check stripe customer
|
||||||
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
|
mainCustomer.processor?.id
|
||||||
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
|
assert.notEqual(stripeCus.discount, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
let curTime = new Date();
|
||||||
|
it("customer should have discount for first purchase", async function () {
|
||||||
|
await autumn.attach({
|
||||||
|
customerId: mainCustomerId,
|
||||||
|
productId: products.proWithTrial.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await timeout(3000);
|
||||||
|
|
||||||
|
curTime = addHours(addDays(curTime, 7), 2);
|
||||||
|
await advanceTestClock({
|
||||||
|
testClockId,
|
||||||
|
advanceTo: curTime.getTime(),
|
||||||
|
stripeCli,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Get invoice
|
||||||
|
let { invoices } = await autumn.customers.get(mainCustomerId);
|
||||||
|
|
||||||
|
assert.equal(invoices.length, 2);
|
||||||
|
assert.equal(invoices[0].total, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customer should have discount for second purchase", async function () {
|
||||||
|
// 2. Check that customer has another discount
|
||||||
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
|
mainCustomer.processor?.id
|
||||||
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
|
assert.notEqual(stripeCus.discount, null);
|
||||||
|
|
||||||
|
// 2. Advance test clock to 1 month from start (trigger discount.deleted event)
|
||||||
|
curTime = addHours(addMonths(new Date(), 1), 2);
|
||||||
|
await advanceTestClock({
|
||||||
|
testClockId,
|
||||||
|
advanceTo: curTime.getTime(),
|
||||||
|
stripeCli,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Advance test clock to 1 month + 7 days from start (trigger new invoice)
|
||||||
|
curTime = addDays(curTime, 7);
|
||||||
|
await advanceTestClock({
|
||||||
|
testClockId,
|
||||||
|
advanceTo: curTime.getTime(),
|
||||||
|
stripeCli,
|
||||||
|
});
|
||||||
|
|
||||||
|
// // 3. Get invoice again
|
||||||
|
let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
|
||||||
|
|
||||||
|
assert.equal(invoices2.length, 3);
|
||||||
|
assert.equal(invoices2[0].total, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { el } from "date-fns/locale";
|
import { el } from "date-fns/locale";
|
||||||
import { getAxiosInstance } from "../utils/setup.js";
|
import { getAxiosInstance } from "../utils/setup.js";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import { AppEnv, CreateCoupon } from "@autumn/shared";
|
import { AppEnv, CreateReward } from "@autumn/shared";
|
||||||
const handleAxiosError = (error: any) => {
|
const handleAxiosError = (error: any) => {
|
||||||
if (error.response.data) {
|
if (error.response.data) {
|
||||||
// console.log(error.response.data);
|
// console.log(error.response.data);
|
||||||
@@ -212,7 +212,7 @@ export class AutumnCli {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createCoupon(coupon: CreateCoupon) {
|
static async createCoupon(coupon: CreateReward) {
|
||||||
const axiosInstance = getAxiosInstance();
|
const axiosInstance = getAxiosInstance();
|
||||||
const { data } = await axiosInstance.post(`/v1/coupons`, coupon);
|
const { data } = await axiosInstance.post(`/v1/coupons`, coupon);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -5,17 +5,21 @@ import {
|
|||||||
AllowanceType,
|
AllowanceType,
|
||||||
AppEnv,
|
AppEnv,
|
||||||
BillingInterval,
|
BillingInterval,
|
||||||
|
CouponDurationType,
|
||||||
|
DiscountType,
|
||||||
EntInterval,
|
EntInterval,
|
||||||
Feature,
|
Feature,
|
||||||
|
RewardTriggerEvent,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { FeatureType } from "@autumn/shared";
|
import { FeatureType } from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
initCoupon,
|
initReward,
|
||||||
initEntitlement,
|
initEntitlement,
|
||||||
initFeature,
|
initFeature,
|
||||||
initFreeTrial,
|
initFreeTrial,
|
||||||
initPrice,
|
initPrice,
|
||||||
initProduct,
|
initProduct,
|
||||||
|
initRewardTrigger,
|
||||||
} from "./utils/init.js";
|
} from "./utils/init.js";
|
||||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
@@ -762,27 +766,49 @@ export const entityProducts = {
|
|||||||
oneTier: true,
|
oneTier: true,
|
||||||
billingUnits: 1,
|
billingUnits: 1,
|
||||||
// Carry over usage
|
// Carry over usage
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
freeTrial: null,
|
freeTrial: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const coupons = {
|
export const rewards = {
|
||||||
rolloverAll: initCoupon({
|
rolloverAll: initReward({
|
||||||
id: "rolloverAll",
|
id: "rolloverAll",
|
||||||
discountValue: 1000,
|
discountValue: 1000,
|
||||||
rollover: true,
|
rollover: true,
|
||||||
applyToAll: true,
|
applyToAll: true,
|
||||||
}),
|
}),
|
||||||
rolloverUsage: initCoupon({
|
rolloverUsage: initReward({
|
||||||
id: "rolloverUsage",
|
id: "rolloverUsage",
|
||||||
discountValue: 1000,
|
discountValue: 1000,
|
||||||
rollover: true,
|
rollover: true,
|
||||||
onlyUsagePrices: true,
|
onlyUsagePrices: true,
|
||||||
productIds: [products.proWithOverage.id],
|
productIds: [products.proWithOverage.id],
|
||||||
}),
|
}),
|
||||||
|
monthOff: initReward({
|
||||||
|
id: "monthOff",
|
||||||
|
discountType: DiscountType.Percentage,
|
||||||
|
discountValue: 100,
|
||||||
|
applyToAll: true,
|
||||||
|
durationType: CouponDurationType.Months,
|
||||||
|
durationValue: 1,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const referralPrograms = {
|
||||||
|
onCheckout: initRewardTrigger({
|
||||||
|
id: "onCheckout",
|
||||||
|
internalRewardId: rewards.monthOff.id,
|
||||||
|
when: RewardTriggerEvent.Checkout,
|
||||||
|
productIds: [products.pro.id, products.proWithTrial.id],
|
||||||
|
}),
|
||||||
|
immediate: initRewardTrigger({
|
||||||
|
id: "immediate",
|
||||||
|
internalRewardId: rewards.monthOff.id,
|
||||||
|
when: RewardTriggerEvent.Immediately,
|
||||||
|
// productIds: [products.pro.id, products.proWithTrial.id],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ORG_SLUG = "unit-test-org";
|
const ORG_SLUG = "unit-test-org";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
FreeTrial,
|
FreeTrial,
|
||||||
Organization,
|
Organization,
|
||||||
PriceType,
|
PriceType,
|
||||||
|
RewardTriggerEvent,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { getAxiosInstance } from "./setup.js";
|
import { getAxiosInstance } from "./setup.js";
|
||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
@@ -326,8 +327,8 @@ export const initCustomer = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Init Coupon
|
// Init Reward
|
||||||
export const initCoupon = ({
|
export const initReward = ({
|
||||||
id,
|
id,
|
||||||
discountType = DiscountType.Fixed,
|
discountType = DiscountType.Fixed,
|
||||||
discountValue,
|
discountValue,
|
||||||
@@ -361,3 +362,25 @@ export const initCoupon = ({
|
|||||||
product_ids: productIds,
|
product_ids: productIds,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initRewardTrigger = ({
|
||||||
|
id,
|
||||||
|
when = RewardTriggerEvent.Immediately,
|
||||||
|
productIds = [],
|
||||||
|
internalRewardId,
|
||||||
|
maxRedemptions = 2,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
productIds?: string[];
|
||||||
|
internalRewardId: string;
|
||||||
|
when?: RewardTriggerEvent;
|
||||||
|
maxRedemptions?: number;
|
||||||
|
}): any => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
when,
|
||||||
|
product_ids: productIds,
|
||||||
|
internal_reward_id: internalRewardId,
|
||||||
|
max_redemptions: maxRedemptions,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||||
import {
|
import {
|
||||||
AppEnv,
|
AppEnv,
|
||||||
CreateCoupon,
|
CreateReward,
|
||||||
Feature,
|
Feature,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
FullProduct,
|
FullProduct,
|
||||||
Price,
|
Price,
|
||||||
PriceType,
|
PriceType,
|
||||||
|
Reward,
|
||||||
|
RewardTrigger,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ import {
|
|||||||
} from "./stripeUtils.js";
|
} from "./stripeUtils.js";
|
||||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
import { Autumn } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js";
|
||||||
|
|
||||||
export const getAxiosInstance = (
|
export const getAxiosInstance = (
|
||||||
apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!
|
apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!
|
||||||
@@ -168,11 +172,15 @@ export const clearOrg = async ({
|
|||||||
await deleteAllStripeTestClocks({ stripeCli });
|
await deleteAllStripeTestClocks({ stripeCli });
|
||||||
console.log(" ✅ Deleted Stripe test clocks");
|
console.log(" ✅ Deleted Stripe test clocks");
|
||||||
|
|
||||||
|
// Delete all stripe meters
|
||||||
|
await deactivateStripeMeters({ org, env });
|
||||||
|
console.log(" ✅ Deactivated Stripe meters");
|
||||||
|
|
||||||
// Batch delete coupons
|
// Batch delete coupons
|
||||||
|
|
||||||
const batchDeleteCoupons = [];
|
const batchDeleteCoupons = [];
|
||||||
const { data: coupons, error: couponError } = await sb
|
const { data: coupons, error: couponError } = await sb
|
||||||
.from("coupons")
|
.from("rewards")
|
||||||
.delete()
|
.delete()
|
||||||
.eq("org_id", orgId)
|
.eq("org_id", orgId)
|
||||||
.eq("env", env)
|
.eq("env", env)
|
||||||
@@ -207,16 +215,19 @@ export const setupOrg = async ({
|
|||||||
env,
|
env,
|
||||||
features,
|
features,
|
||||||
products,
|
products,
|
||||||
coupons,
|
rewards,
|
||||||
|
rewardTriggers,
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
features: Record<string, Feature & { eventName: string }>;
|
features: Record<string, Feature & { eventName: string }>;
|
||||||
products: Record<string, FullProduct | any>;
|
products: Record<string, FullProduct | any>;
|
||||||
coupons: Record<string, any>;
|
rewards: Record<string, any>;
|
||||||
|
rewardTriggers: Record<string, RewardTrigger>;
|
||||||
}) => {
|
}) => {
|
||||||
const axiosInstance = getAxiosInstance();
|
const axiosInstance = getAxiosInstance();
|
||||||
const sb = createSupabaseClient();
|
const sb = createSupabaseClient();
|
||||||
|
const autumn = new Autumn(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!);
|
||||||
|
|
||||||
let insertFeatures = [];
|
let insertFeatures = [];
|
||||||
for (const feature of Object.values(features)) {
|
for (const feature of Object.values(features)) {
|
||||||
@@ -298,14 +309,14 @@ export const setupOrg = async ({
|
|||||||
|
|
||||||
// Insert coupons
|
// Insert coupons
|
||||||
let insertCoupons = [];
|
let insertCoupons = [];
|
||||||
for (const coupon of Object.values(coupons)) {
|
for (const reward of Object.values(rewards)) {
|
||||||
const createCoupon = async () => {
|
const createCoupon = async () => {
|
||||||
let priceIds = [];
|
let priceIds = [];
|
||||||
|
|
||||||
if (coupon.only_usage_prices) {
|
if (reward.only_usage_prices) {
|
||||||
let filteredProducts = allProducts.filter((product: FullProduct) => {
|
let filteredProducts = allProducts.filter((product: FullProduct) => {
|
||||||
if (coupon.product_ids) {
|
if (reward.product_ids) {
|
||||||
return coupon.product_ids.includes(product.id);
|
return reward.product_ids.includes(product.id);
|
||||||
} else return true;
|
} else return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,44 +327,56 @@ export const setupOrg = async ({
|
|||||||
return price.id;
|
return price.id;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (coupon.product_ids) {
|
} else if (reward.product_ids) {
|
||||||
priceIds = allProducts
|
priceIds = allProducts
|
||||||
.filter((product: FullProduct) =>
|
.filter((product: FullProduct) =>
|
||||||
coupon.product_ids.includes(product.id)
|
reward.product_ids.includes(product.id)
|
||||||
)
|
)
|
||||||
.flatMap((product: FullProduct) =>
|
.flatMap((product: FullProduct) =>
|
||||||
product.prices.map((price) => price.id)
|
product.prices.map((price) => price.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCoupon: CreateCoupon & { id: string } = {
|
const newReward: CreateReward & { id: string } = {
|
||||||
id: coupon.id,
|
id: reward.id,
|
||||||
name: coupon.name,
|
name: reward.name,
|
||||||
price_ids: priceIds,
|
price_ids: priceIds,
|
||||||
promo_codes: [
|
promo_codes: [
|
||||||
{
|
{
|
||||||
code: coupon.id,
|
code: reward.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
discount_type: coupon.discount_type,
|
discount_type: reward.discount_type,
|
||||||
discount_value: coupon.discount_value,
|
discount_value: reward.discount_value,
|
||||||
duration_type: coupon.duration_type,
|
duration_type: reward.duration_type,
|
||||||
duration_value: coupon.duration_value,
|
duration_value: reward.duration_value,
|
||||||
should_rollover: coupon.should_rollover,
|
should_rollover: reward.should_rollover,
|
||||||
apply_to_all: coupon.apply_to_all,
|
apply_to_all: reward.apply_to_all,
|
||||||
};
|
};
|
||||||
|
|
||||||
let couponRes = await AutumnCli.createCoupon(newCoupon);
|
let rewardRes = await autumn.rewards.create(newReward);
|
||||||
return {
|
return {
|
||||||
id: coupon.id,
|
id: reward.id,
|
||||||
couponRes,
|
rewardRes,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
insertCoupons.push(createCoupon());
|
insertCoupons.push(createCoupon());
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(insertCoupons);
|
await Promise.all(insertCoupons);
|
||||||
console.log("✅ Inserted coupons");
|
console.log("✅ Inserted coupons");
|
||||||
|
|
||||||
|
// CREATE REWARD TRIGGERS
|
||||||
|
let insertRewardTriggers = [];
|
||||||
|
for (const rewardTrigger of Object.values(rewardTriggers)) {
|
||||||
|
const createRewardTrigger = async () => {
|
||||||
|
await autumn.referralPrograms.create(rewardTrigger);
|
||||||
|
};
|
||||||
|
insertRewardTriggers.push(createRewardTrigger());
|
||||||
|
}
|
||||||
|
await Promise.all(insertRewardTriggers);
|
||||||
|
console.log("✅ Inserted reward triggers");
|
||||||
|
|
||||||
// Initialize stripe products
|
// Initialize stripe products
|
||||||
// How to check if mocha is in parallel mode?
|
// How to check if mocha is in parallel mode?
|
||||||
if (process.env.MOCHA_PARALLEL) {
|
if (process.env.MOCHA_PARALLEL) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export enum DiscountType {
|
|||||||
Fixed = "fixed",
|
Fixed = "fixed",
|
||||||
}
|
}
|
||||||
|
|
||||||
const CouponSchema = z.object({
|
const RewardSchema = z.object({
|
||||||
internal_id: z.string(),
|
internal_id: z.string(),
|
||||||
name: z.string().nullish(),
|
name: z.string().nullish(),
|
||||||
price_ids: z.array(z.string()),
|
price_ids: z.array(z.string()),
|
||||||
@@ -31,13 +31,12 @@ const CouponSchema = z.object({
|
|||||||
created_at: z.number(),
|
created_at: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateCouponSchema = CouponSchema.omit({
|
export const CreateRewardSchema = RewardSchema.omit({
|
||||||
internal_id: true,
|
internal_id: true,
|
||||||
org_id: true,
|
org_id: true,
|
||||||
env: true,
|
env: true,
|
||||||
created_at: true,
|
created_at: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Coupon = z.infer<typeof CouponSchema>;
|
export type CreateReward = z.infer<typeof CreateRewardSchema>;
|
||||||
export type CreateCoupon = z.infer<typeof CreateCouponSchema>;
|
export type Reward = z.infer<typeof RewardSchema>;
|
||||||
export type Reward = z.infer<typeof CouponSchema>;
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { Coupon, CreateCoupon } from "@autumn/shared";
|
import { Reward, CreateReward } from "@autumn/shared";
|
||||||
|
|
||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import { AxiosInstance } from "axios";
|
import { AxiosInstance } from "axios";
|
||||||
|
|
||||||
export class CouponService {
|
export class RewardService {
|
||||||
static async createCoupon({
|
static async createReward({
|
||||||
axiosInstance,
|
axiosInstance,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
axiosInstance: AxiosInstance;
|
axiosInstance: AxiosInstance;
|
||||||
data: CreateCoupon;
|
data: CreateReward;
|
||||||
}) {
|
}) {
|
||||||
await axiosInstance.post("/v1/rewards", data);
|
await axiosInstance.post("/v1/rewards", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deleteCoupon({
|
static async deleteReward({
|
||||||
axiosInstance,
|
axiosInstance,
|
||||||
internalId,
|
internalId,
|
||||||
}: {
|
}: {
|
||||||
@@ -24,14 +24,14 @@ export class CouponService {
|
|||||||
await axiosInstance.delete(`/v1/rewards/${internalId}`);
|
await axiosInstance.delete(`/v1/rewards/${internalId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateCoupon({
|
static async updateReward({
|
||||||
axiosInstance,
|
axiosInstance,
|
||||||
internalId,
|
internalId,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
axiosInstance: AxiosInstance;
|
axiosInstance: AxiosInstance;
|
||||||
internalId: string;
|
internalId: string;
|
||||||
data: Coupon;
|
data: Reward;
|
||||||
}) {
|
}) {
|
||||||
await axiosInstance.post(`/v1/rewards/${internalId}`, data);
|
await axiosInstance.post(`/v1/rewards/${internalId}`, data);
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export const CustomerToolbar = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex text-sm items-center justify-between w-full gap-2">
|
<div className="flex text-sm items-center justify-between w-full gap-2">
|
||||||
<p className="text-t2">Add Coupon</p>
|
<p className="text-t2">Add Reward</p>
|
||||||
<Ticket size={12} className="text-t3" />
|
<Ticket size={12} className="text-t3" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DialogFooter } from "@/components/ui/dialog";
|
|||||||
|
|
||||||
import { getOriginalCouponId } from "@/utils/product/couponUtils";
|
import { getOriginalCouponId } from "@/utils/product/couponUtils";
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { Coupon, CreateCustomer, Customer } from "@autumn/shared";
|
import { Reward, CreateCustomer, Customer } from "@autumn/shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogTitle } from "@/components/ui/dialog";
|
import { DialogTitle } from "@/components/ui/dialog";
|
||||||
@@ -29,7 +29,7 @@ const UpdateCustomerDialog = ({
|
|||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { cusMutate } = useCustomerContext();
|
const { cusMutate } = useCustomerContext();
|
||||||
const [couponSelected, setCouponSelected] = useState<Coupon | null>(null);
|
const [couponSelected, setCouponSelected] = useState<Reward | null>(null);
|
||||||
const [customer, setCustomer] = useState<CreateCustomer>(selectedCustomer);
|
const [customer, setCustomer] = useState<CreateCustomer>(selectedCustomer);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const env = useEnv();
|
const env = useEnv();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SelectContent } from "@/components/ui/select";
|
|||||||
import { SelectValue } from "@/components/ui/select";
|
import { SelectValue } from "@/components/ui/select";
|
||||||
import { SelectTrigger } from "@/components/ui/select";
|
import { SelectTrigger } from "@/components/ui/select";
|
||||||
import { DialogFooter } from "@/components/ui/dialog";
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Coupon } from "@autumn/shared";
|
import { Reward } from "@autumn/shared";
|
||||||
import { Select, SelectItem } from "@/components/ui/select";
|
import { Select, SelectItem } from "@/components/ui/select";
|
||||||
import { DialogContent, DialogTitle } from "@/components/ui/dialog";
|
import { DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -22,7 +22,7 @@ const AddCouponDialogContent = ({
|
|||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { cusMutate, customer, coupons, discount } = useCustomerContext();
|
const { cusMutate, customer, coupons, discount } = useCustomerContext();
|
||||||
const [couponSelected, setCouponSelected] = useState<Coupon | null>(null);
|
const [couponSelected, setCouponSelected] = useState<Reward | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const env = useEnv();
|
const env = useEnv();
|
||||||
const axiosInstance = useAxiosInstance({ env });
|
const axiosInstance = useAxiosInstance({ env });
|
||||||
@@ -38,7 +38,7 @@ const AddCouponDialogContent = ({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
await cusMutate();
|
await cusMutate();
|
||||||
|
|
||||||
toast.success("Coupon added to customer");
|
toast.success("Reward added to customer");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(getBackendErr(error, "Failed to create coupon"));
|
toast.error(getBackendErr(error, "Failed to create coupon"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -51,7 +51,7 @@ const AddCouponDialogContent = ({
|
|||||||
const getExistingCoupon = () => {
|
const getExistingCoupon = () => {
|
||||||
if (discount) {
|
if (discount) {
|
||||||
return coupons.find(
|
return coupons.find(
|
||||||
(c: Coupon) => c.internal_id === getOriginalCouponId(discount.coupon.id)
|
(c: Reward) => c.internal_id === getOriginalCouponId(discount.coupon.id)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@@ -60,10 +60,10 @@ const AddCouponDialogContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Add Coupon</DialogTitle>
|
<DialogTitle>Add Reward</DialogTitle>
|
||||||
{getExistingCoupon() && (
|
{getExistingCoupon() && (
|
||||||
<WarningBox>
|
<WarningBox>
|
||||||
Coupon {getExistingCoupon()?.name} already applied. Adding a new one
|
Reward {getExistingCoupon()?.name} already applied. Adding a new one
|
||||||
will replace the existing one.
|
will replace the existing one.
|
||||||
</WarningBox>
|
</WarningBox>
|
||||||
)}
|
)}
|
||||||
@@ -71,20 +71,20 @@ const AddCouponDialogContent = ({
|
|||||||
<Select
|
<Select
|
||||||
value={couponSelected?.internal_id}
|
value={couponSelected?.internal_id}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const coupon = coupons.find((c: Coupon) => c.internal_id === value);
|
const coupon = coupons.find((c: Reward) => c.internal_id === value);
|
||||||
if (coupon) {
|
if (coupon) {
|
||||||
setCouponSelected(coupon);
|
setCouponSelected(coupon);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select Coupon" />
|
<SelectValue placeholder="Select Reward" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{/* If empty */}
|
{/* If empty */}
|
||||||
|
|
||||||
{coupons && coupons.length > 0 ? (
|
{coupons && coupons.length > 0 ? (
|
||||||
coupons.map((coupon: Coupon) => (
|
coupons.map((coupon: Reward) => (
|
||||||
<SelectItem key={coupon.internal_id} value={coupon.internal_id}>
|
<SelectItem key={coupon.internal_id} value={coupon.internal_id}>
|
||||||
{coupon.name}
|
{coupon.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -104,7 +104,7 @@ const AddCouponDialogContent = ({
|
|||||||
disabled={!couponSelected}
|
disabled={!couponSelected}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
>
|
>
|
||||||
Add Coupon
|
Add Reward
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ import { ProductsContext } from "./ProductsContext";
|
|||||||
import { AppEnv } from "@autumn/shared";
|
import { AppEnv } from "@autumn/shared";
|
||||||
import CreateProduct from "./CreateProduct";
|
import CreateProduct from "./CreateProduct";
|
||||||
import { ProductsTable } from "./ProductsTable";
|
import { ProductsTable } from "./ProductsTable";
|
||||||
import { CouponsTable } from "./coupons/CouponsTable";
|
|
||||||
import CreateCoupon from "./coupons/CreateCoupon";
|
|
||||||
import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
|
import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
|
||||||
|
|
||||||
import { Ticket } from "lucide-react";
|
import { Ticket } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CreateRewardTrigger from "./reward-triggers/CreateRewardTriger";
|
|
||||||
import { RewardTriggersTable } from "./reward-triggers/RewardTriggersTable";
|
import { RewardTriggersTable } from "./reward-triggers/RewardTriggersTable";
|
||||||
import CreateRewardTriggerModal from "./reward-triggers/CreateRewardTriger";
|
import CreateRewardTriggerModal from "./reward-triggers/CreateRewardTriger";
|
||||||
|
import { RewardsTable } from "./rewards/RewardsTAble";
|
||||||
|
import CreateReward from "./rewards/CreateReward";
|
||||||
|
|
||||||
function ProductsView({ env }: { env: AppEnv }) {
|
function ProductsView({ env }: { env: AppEnv }) {
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||||
const [showCoupons, setShowCoupons] = useState(false);
|
const [showRewards, setShowRewards] = useState(false);
|
||||||
const { data, isLoading, mutate } = useAxiosSWR({
|
const { data, isLoading, mutate } = useAxiosSWR({
|
||||||
url: `/products/data`,
|
url: `/products/data`,
|
||||||
env: env,
|
env: env,
|
||||||
@@ -38,8 +38,8 @@ function ProductsView({ env }: { env: AppEnv }) {
|
|||||||
setSelectedProduct(data.products[0]);
|
setSelectedProduct(data.products[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.coupons.length > 0) {
|
if (data?.rewards.length > 0) {
|
||||||
setShowCoupons(true);
|
setShowRewards(true);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ function ProductsView({ env }: { env: AppEnv }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ToggleDisplayButton
|
<ToggleDisplayButton
|
||||||
show={showCoupons}
|
show={showRewards}
|
||||||
disabled={data?.coupons.length > 0}
|
disabled={data?.rewards.length > 0}
|
||||||
onClick={() => setShowCoupons((prev) => !prev)}
|
onClick={() => setShowRewards((prev) => !prev)}
|
||||||
>
|
>
|
||||||
<Ticket size={12} className="mr-2" />
|
<Ticket size={12} className="mr-2" />
|
||||||
Coupons
|
Coupons
|
||||||
@@ -75,7 +75,7 @@ function ProductsView({ env }: { env: AppEnv }) {
|
|||||||
</div>
|
</div>
|
||||||
<ProductsTable products={data?.products} />
|
<ProductsTable products={data?.products} />
|
||||||
<CreateProduct />
|
<CreateProduct />
|
||||||
{showCoupons && (
|
{showRewards && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="flex flex-col gap-4 h-fit mt-6">
|
<div className="flex flex-col gap-4 h-fit mt-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -86,8 +86,8 @@ function ProductsView({ env }: { env: AppEnv }) {
|
|||||||
{/* <span className="text-t3">(eg, 10% off all products).</span> */}
|
{/* <span className="text-t3">(eg, 10% off all products).</span> */}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CouponsTable />
|
<RewardsTable />
|
||||||
<CreateCoupon />
|
<CreateReward />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 h-fit mt-6">
|
<div className="flex flex-col gap-4 h-fit mt-6">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Coupon,
|
Reward,
|
||||||
CouponDurationType,
|
CouponDurationType,
|
||||||
CreateCoupon as CreateCouponType,
|
CreateReward as CreateCouponType,
|
||||||
DiscountType,
|
DiscountType,
|
||||||
RewardTrigger,
|
RewardTrigger,
|
||||||
RewardTriggerEvent,
|
RewardTriggerEvent,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { RewardTrigger, Coupon, RewardTriggerEvent } from "@autumn/shared";
|
import { RewardTrigger, Reward, RewardTriggerEvent } from "@autumn/shared";
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -50,7 +50,7 @@ export const RewardTriggerConfig = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-6/12">
|
<div className="w-6/12">
|
||||||
<FieldLabel>Coupon</FieldLabel>
|
<FieldLabel>Reward</FieldLabel>
|
||||||
<Select
|
<Select
|
||||||
value={rewardTrigger.internal_reward_id}
|
value={rewardTrigger.internal_reward_id}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -61,7 +61,7 @@ export const RewardTriggerConfig = ({
|
|||||||
<SelectValue placeholder="Select a coupon" />
|
<SelectValue placeholder="Select a coupon" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{coupons.map((coupon: Coupon) => (
|
{coupons.map((coupon: Reward) => (
|
||||||
<SelectItem key={coupon.name} value={coupon.internal_id}>
|
<SelectItem key={coupon.name} value={coupon.internal_id}>
|
||||||
{coupon.name}
|
{coupon.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
|||||||
import { TableCell } from "@/components/ui/table";
|
import { TableCell } from "@/components/ui/table";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Coupon,
|
Reward,
|
||||||
CouponDurationType,
|
CouponDurationType,
|
||||||
DiscountType,
|
DiscountType,
|
||||||
RewardTrigger,
|
RewardTrigger,
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
|
||||||
import { SelectContent } from "@/components/ui/select";
|
|
||||||
import { SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { SelectItem } from "@/components/ui/select";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -19,18 +15,18 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Coupon,
|
Reward,
|
||||||
CouponDurationType,
|
CouponDurationType,
|
||||||
CreateCoupon as CreateCouponType,
|
CreateReward as CreateRewardType,
|
||||||
DiscountType,
|
DiscountType,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
|
import { RewardService } from "@/services/products/RewardService";
|
||||||
|
import { RewardConfig } from "./RewardConfig";
|
||||||
|
|
||||||
import { CouponConfig } from "./CouponConfig";
|
const defaultReward: CreateRewardType = {
|
||||||
import { CouponService } from "@/services/products/CouponService";
|
|
||||||
|
|
||||||
const defaultCoupon: CreateCouponType = {
|
|
||||||
name: "",
|
name: "",
|
||||||
promo_codes: [{ code: "" }],
|
promo_codes: [{ code: "" }],
|
||||||
price_ids: [],
|
price_ids: [],
|
||||||
@@ -42,27 +38,27 @@ const defaultCoupon: CreateCouponType = {
|
|||||||
apply_to_all: true,
|
apply_to_all: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CreateCoupon() {
|
function CreateReward() {
|
||||||
const { mutate, env } = useProductsContext();
|
const { mutate, env } = useProductsContext();
|
||||||
const axiosInstance = useAxiosInstance({ env: env });
|
const axiosInstance = useAxiosInstance({ env: env });
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const [coupon, setCoupon] = useState(defaultCoupon);
|
const [reward, setReward] = useState(defaultReward);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setCoupon(defaultCoupon);
|
setReward(defaultReward);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await CouponService.createCoupon({
|
await RewardService.createReward({
|
||||||
axiosInstance,
|
axiosInstance,
|
||||||
data: coupon,
|
data: reward,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mutate();
|
await mutate();
|
||||||
@@ -81,18 +77,18 @@ function CreateCoupon() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
startIcon={<PlusIcon size={15} />}
|
startIcon={<PlusIcon size={15} />}
|
||||||
>
|
>
|
||||||
Create Coupon
|
Create Reward
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="w-[500px]">
|
<DialogContent className="w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Coupon</DialogTitle>
|
<DialogTitle>Create Reward</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* <CreditSystemConfig
|
{/* <CreditSystemConfig
|
||||||
creditSystem={creditSystem}
|
creditSystem={creditSystem}
|
||||||
setCreditSystem={setCreditSystem}
|
setCreditSystem={setCreditSystem}
|
||||||
/> */}
|
/> */}
|
||||||
<CouponConfig coupon={coupon as any} setCoupon={setCoupon} />
|
<RewardConfig reward={reward as any} setReward={setReward} />
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
@@ -107,4 +103,4 @@ function CreateCoupon() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateCoupon;
|
export default CreateReward;
|
||||||
@@ -4,14 +4,8 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
|
||||||
import { SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||||
import {
|
import { Reward, CouponDurationType, DiscountType } from "@autumn/shared";
|
||||||
Coupon,
|
|
||||||
CouponDurationType,
|
|
||||||
CreateCoupon,
|
|
||||||
DiscountType,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -31,12 +25,12 @@ import { Check, ChevronsUpDown, Trash2, X } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
export const CouponConfig = ({
|
export const RewardConfig = ({
|
||||||
coupon,
|
reward,
|
||||||
setCoupon,
|
setReward,
|
||||||
}: {
|
}: {
|
||||||
coupon: Coupon;
|
reward: Reward;
|
||||||
setCoupon: (coupon: Coupon) => void;
|
setReward: (reward: Reward) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { org } = useProductsContext();
|
const { org } = useProductsContext();
|
||||||
return (
|
return (
|
||||||
@@ -45,8 +39,8 @@ export const CouponConfig = ({
|
|||||||
<div className="w-6/12">
|
<div className="w-6/12">
|
||||||
<FieldLabel description="Will be shown on receipt">Name</FieldLabel>
|
<FieldLabel description="Will be shown on receipt">Name</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
value={coupon.name || ""}
|
value={reward.name || ""}
|
||||||
onChange={(e) => setCoupon({ ...coupon, name: e.target.value })}
|
onChange={(e) => setReward({ ...reward, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-6/12">
|
<div className="w-6/12">
|
||||||
@@ -55,11 +49,11 @@ export const CouponConfig = ({
|
|||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
value={
|
value={
|
||||||
coupon.promo_codes.length > 0 ? coupon.promo_codes[0].code : ""
|
reward.promo_codes.length > 0 ? reward.promo_codes[0].code : ""
|
||||||
}
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCoupon({
|
setReward({
|
||||||
...coupon,
|
...reward,
|
||||||
promo_codes: [{ code: e.target.value }],
|
promo_codes: [{ code: e.target.value }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -70,9 +64,9 @@ export const CouponConfig = ({
|
|||||||
<div className="w-6/12">
|
<div className="w-6/12">
|
||||||
<FieldLabel>Discount Type</FieldLabel>
|
<FieldLabel>Discount Type</FieldLabel>
|
||||||
<Select
|
<Select
|
||||||
value={coupon.discount_type}
|
value={reward.discount_type}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setCoupon({ ...coupon, discount_type: value as DiscountType })
|
setReward({ ...reward, discount_type: value as DiscountType })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -90,13 +84,13 @@ export const CouponConfig = ({
|
|||||||
<div className="w-6/12">
|
<div className="w-6/12">
|
||||||
<FieldLabel>Amount</FieldLabel>
|
<FieldLabel>Amount</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
value={coupon.discount_value}
|
value={reward.discount_value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCoupon({ ...coupon, discount_value: Number(e.target.value) })
|
setReward({ ...reward, discount_value: Number(e.target.value) })
|
||||||
}
|
}
|
||||||
endContent={
|
endContent={
|
||||||
<p className="text-t3">
|
<p className="text-t3">
|
||||||
{coupon.discount_type === DiscountType.Percentage
|
{reward.discount_type === DiscountType.Percentage
|
||||||
? "%"
|
? "%"
|
||||||
: org?.currency || "USD"}
|
: org?.currency || "USD"}
|
||||||
</p>
|
</p>
|
||||||
@@ -108,13 +102,13 @@ export const CouponConfig = ({
|
|||||||
<div className="w-6/12">
|
<div className="w-6/12">
|
||||||
<FieldLabel>Duration</FieldLabel>
|
<FieldLabel>Duration</FieldLabel>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{coupon.duration_type === CouponDurationType.Months && (
|
{reward.duration_type === CouponDurationType.Months && (
|
||||||
<Input
|
<Input
|
||||||
className="w-[60px] no-spinner"
|
className="w-[60px] no-spinner"
|
||||||
value={coupon.duration_value}
|
value={reward.duration_value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCoupon({
|
setReward({
|
||||||
...coupon,
|
...reward,
|
||||||
duration_value: Number(e.target.value),
|
duration_value: Number(e.target.value),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -122,10 +116,10 @@ export const CouponConfig = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Select
|
<Select
|
||||||
value={coupon.duration_type}
|
value={reward.duration_type}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setCoupon({
|
setReward({
|
||||||
...coupon,
|
...reward,
|
||||||
duration_type: value as CouponDurationType,
|
duration_type: value as CouponDurationType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -144,13 +138,13 @@ export const CouponConfig = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{coupon.duration_type === CouponDurationType.OneOff && (
|
{reward.duration_type === CouponDurationType.OneOff && (
|
||||||
<div className="w-full ml-1 flex items-center gap-2">
|
<div className="w-full ml-1 flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={coupon.should_rollover}
|
checked={reward.should_rollover}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setCoupon({
|
setReward({
|
||||||
...coupon,
|
...reward,
|
||||||
should_rollover: checked === true,
|
should_rollover: checked === true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -162,31 +156,31 @@ export const CouponConfig = ({
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="text-t2 mb-2">Products</p>
|
<p className="text-t2 mb-2">Products</p>
|
||||||
|
|
||||||
<ProductPriceSelector coupon={coupon} setCoupon={setCoupon} />
|
<ProductPriceSelector reward={reward} setReward={setReward} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProductPriceSelector = ({
|
const ProductPriceSelector = ({
|
||||||
coupon,
|
reward,
|
||||||
setCoupon,
|
setReward,
|
||||||
}: {
|
}: {
|
||||||
coupon: Coupon;
|
reward: Reward;
|
||||||
setCoupon: (coupon: Coupon) => void;
|
setReward: (reward: Reward) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { products, features } = useProductsContext();
|
const { products, features } = useProductsContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
// Handle selection/deselection of a price
|
// Handle selection/deselection of a price
|
||||||
const handlePriceToggle = (priceId: string) => {
|
const handlePriceToggle = (priceId: string) => {
|
||||||
let newPriceIds = [...coupon.price_ids];
|
let newPriceIds = [...reward.price_ids];
|
||||||
if (coupon.price_ids.includes(priceId)) {
|
if (reward.price_ids.includes(priceId)) {
|
||||||
newPriceIds = coupon.price_ids.filter((id) => id !== priceId);
|
newPriceIds = reward.price_ids.filter((id) => id !== priceId);
|
||||||
} else {
|
} else {
|
||||||
newPriceIds = [...coupon.price_ids, priceId];
|
newPriceIds = [...reward.price_ids, priceId];
|
||||||
}
|
}
|
||||||
setCoupon({ ...coupon, price_ids: newPriceIds });
|
setReward({ ...reward, price_ids: newPriceIds });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!products || products.length === 0) {
|
if (!products || products.length === 0) {
|
||||||
@@ -212,13 +206,13 @@ const ProductPriceSelector = ({
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-full justify-between min-h-9 flex flex-wrap h-fit py-2 justify-start items-center gap-2 relative hover:bg-zinc-50"
|
className="w-full justify-between min-h-9 flex flex-wrap h-fit py-2 justify-start items-center gap-2 relative hover:bg-zinc-50"
|
||||||
>
|
>
|
||||||
{coupon.apply_to_all ? (
|
{reward.apply_to_all ? (
|
||||||
"All Products"
|
"All Products"
|
||||||
) : coupon.price_ids.length == 0 ? (
|
) : reward.price_ids.length == 0 ? (
|
||||||
"Select Products"
|
"Select Products"
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{coupon.price_ids.map((priceId) => (
|
{reward.price_ids.map((priceId) => (
|
||||||
<div
|
<div
|
||||||
key={priceId}
|
key={priceId}
|
||||||
className="py-1 px-3 text-xs text-t3 border-zinc-300 bg-zinc-100 rounded-full w-fit flex items-center gap-2 h-fit"
|
className="py-1 px-3 text-xs text-t3 border-zinc-300 bg-zinc-100 rounded-full w-fit flex items-center gap-2 h-fit"
|
||||||
@@ -251,20 +245,20 @@ const ProductPriceSelector = ({
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setCoupon({
|
setReward({
|
||||||
...coupon,
|
...reward,
|
||||||
apply_to_all: !coupon.apply_to_all,
|
apply_to_all: !reward.apply_to_all,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<p>Apply to all products</p>
|
<p>Apply to all products</p>
|
||||||
{coupon.apply_to_all && (
|
{reward.apply_to_all && (
|
||||||
<Check size={12} className="text-t3" />
|
<Check size={12} className="text-t3" />
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
{!coupon.apply_to_all &&
|
{!reward.apply_to_all &&
|
||||||
products.map((product: any) => (
|
products.map((product: any) => (
|
||||||
<CommandGroup key={product.id} heading={product.name}>
|
<CommandGroup key={product.id} heading={product.name}>
|
||||||
{product.prices.length > 0 ? (
|
{product.prices.length > 0 ? (
|
||||||
@@ -276,7 +270,7 @@ const ProductPriceSelector = ({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="flex items-center">{price.name}</div>
|
<div className="flex items-center">{price.name}</div>
|
||||||
{coupon.price_ids.includes(price.id) && (
|
{reward.price_ids.includes(price.id) && (
|
||||||
<Check size={12} className="text-t3" />
|
<Check size={12} className="text-t3" />
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
@@ -10,20 +10,14 @@ import { useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
import { Coupon } from "@autumn/shared";
|
import { Reward } from "@autumn/shared";
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
import { CouponService } from "@/services/products/CouponService";
|
import { RewardService } from "@/services/products/RewardService";
|
||||||
import { ToolbarButton } from "@/components/general/table-components/ToolbarButton";
|
import { ToolbarButton } from "@/components/general/table-components/ToolbarButton";
|
||||||
import { Delete } from "lucide-react";
|
import { Delete } from "lucide-react";
|
||||||
|
|
||||||
export const CouponRowToolbar = ({
|
export const RewardRowToolbar = ({ reward }: { reward: Reward }) => {
|
||||||
className,
|
|
||||||
coupon,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
coupon: Coupon;
|
|
||||||
}) => {
|
|
||||||
const { env, mutate } = useProductsContext();
|
const { env, mutate } = useProductsContext();
|
||||||
const axiosInstance = useAxiosInstance({ env });
|
const axiosInstance = useAxiosInstance({ env });
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
@@ -33,9 +27,9 @@ export const CouponRowToolbar = ({
|
|||||||
setDeleteLoading(true);
|
setDeleteLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CouponService.deleteCoupon({
|
await RewardService.deleteReward({
|
||||||
axiosInstance,
|
axiosInstance,
|
||||||
internalId: coupon.internal_id,
|
internalId: reward.internal_id,
|
||||||
});
|
});
|
||||||
await mutate();
|
await mutate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
import { CouponRowToolbar } from "./CouponRowToolbar";
|
|
||||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||||
import { Coupon, CouponDurationType, DiscountType } from "@autumn/shared";
|
import { Reward, CouponDurationType, DiscountType } from "@autumn/shared";
|
||||||
import UpdateCoupon from "./UpdateCoupon";
|
import UpdateReward from "./UpdateReward";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
export const CouponsTable = () => {
|
import { RewardRowToolbar } from "./RewardRowToolbar";
|
||||||
const { coupons, org } = useProductsContext();
|
export const RewardsTable = () => {
|
||||||
const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null);
|
const { rewards, org } = useProductsContext();
|
||||||
|
const [selectedReward, setSelectedReward] = useState<Reward | null>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
// const handleRowClick = (id: string) => {
|
// const handleRowClick = (id: string) => {
|
||||||
@@ -31,11 +31,11 @@ export const CouponsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UpdateCoupon
|
<UpdateReward
|
||||||
open={open}
|
open={open}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
selectedCoupon={selectedCoupon}
|
selectedReward={selectedReward}
|
||||||
setSelectedCoupon={setSelectedCoupon}
|
setSelectedReward={setSelectedReward}
|
||||||
/>
|
/>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="rounded-full">
|
<TableHeader className="rounded-full">
|
||||||
@@ -50,48 +50,48 @@ export const CouponsTable = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{coupons.map((coupon: Coupon) => (
|
{rewards.map((reward: Reward) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={coupon.internal_id}
|
key={reward.internal_id}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedCoupon(coupon);
|
setSelectedReward(reward);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium">{coupon.name}</TableCell>
|
<TableCell className="font-medium">{reward.name}</TableCell>
|
||||||
<TableCell className="font-mono">
|
<TableCell className="font-mono">
|
||||||
{coupon.promo_codes
|
{reward.promo_codes
|
||||||
.map((promoCode) => promoCode.code)
|
.map((promoCode) => promoCode.code)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="min-w-32">
|
<TableCell className="min-w-32">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<p>{coupon.discount_value} </p>
|
<p>{reward.discount_value} </p>
|
||||||
<p className="text-t3">
|
<p className="text-t3">
|
||||||
{coupon.discount_type == DiscountType.Percentage
|
{reward.discount_type == DiscountType.Percentage
|
||||||
? "%"
|
? "%"
|
||||||
: org?.default_currency || "USD"}
|
: org?.default_currency || "USD"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="">
|
<TableCell className="">
|
||||||
{coupon.duration_type == CouponDurationType.Months
|
{reward.duration_type == CouponDurationType.Months
|
||||||
? `${coupon.duration_value} months`
|
? `${reward.duration_value} months`
|
||||||
: coupon.duration_type == CouponDurationType.OneOff &&
|
: reward.duration_type == CouponDurationType.OneOff &&
|
||||||
coupon.should_rollover
|
reward.should_rollover
|
||||||
? "One-off (rollover)"
|
? "One-off (rollover)"
|
||||||
: keyToTitle(coupon.duration_type)}
|
: keyToTitle(reward.duration_type)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="">
|
<TableCell className="">
|
||||||
{formatUnixToDateTime(coupon.created_at).date}
|
{formatUnixToDateTime(reward.created_at).date}
|
||||||
<span className="text-t3">
|
<span className="text-t3">
|
||||||
{" "}
|
{" "}
|
||||||
{formatUnixToDateTime(coupon.created_at).time}{" "}
|
{formatUnixToDateTime(reward.created_at).time}{" "}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="">
|
<TableCell className="">
|
||||||
<CouponRowToolbar coupon={coupon} />
|
<RewardRowToolbar reward={reward} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -9,27 +9,27 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { FeatureService } from "@/services/FeatureService";
|
import { FeatureService } from "@/services/FeatureService";
|
||||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Coupon } from "@autumn/shared";
|
import { Reward } from "@autumn/shared";
|
||||||
import { useEnv } from "@/utils/envUtils";
|
import { useEnv } from "@/utils/envUtils";
|
||||||
import { useProductsContext } from "../ProductsContext";
|
import { useProductsContext } from "../ProductsContext";
|
||||||
import { CouponConfig } from "./CouponConfig";
|
import { RewardConfig } from "./RewardConfig";
|
||||||
import { CouponService } from "@/services/products/CouponService";
|
import { RewardService } from "@/services/products/RewardService";
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
||||||
|
|
||||||
function UpdateCoupon({
|
function UpdateReward({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
selectedCoupon,
|
selectedReward,
|
||||||
setSelectedCoupon,
|
setSelectedReward,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
selectedCoupon: Coupon | null;
|
selectedReward: Reward | null;
|
||||||
setSelectedCoupon: (coupon: Coupon) => void;
|
setSelectedReward: (reward: Reward) => void;
|
||||||
}) {
|
}) {
|
||||||
const [updateLoading, setUpdateLoading] = useState(false);
|
const [updateLoading, setUpdateLoading] = useState(false);
|
||||||
const { coupons, mutate } = useProductsContext();
|
const { rewards, mutate } = useProductsContext();
|
||||||
|
|
||||||
const env = useEnv();
|
const env = useEnv();
|
||||||
const axiosInstance = useAxiosInstance({ env });
|
const axiosInstance = useAxiosInstance({ env });
|
||||||
@@ -37,12 +37,12 @@ function UpdateCoupon({
|
|||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
setUpdateLoading(true);
|
setUpdateLoading(true);
|
||||||
try {
|
try {
|
||||||
await CouponService.updateCoupon({
|
await RewardService.updateReward({
|
||||||
axiosInstance,
|
axiosInstance,
|
||||||
internalId: selectedCoupon!.internal_id,
|
internalId: selectedReward!.internal_id,
|
||||||
data: selectedCoupon!,
|
data: selectedReward!,
|
||||||
});
|
});
|
||||||
toast.success("Coupon updated successfully");
|
toast.success("Reward updated successfully");
|
||||||
await mutate();
|
await mutate();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -54,13 +54,13 @@ function UpdateCoupon({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Update Coupon</DialogTitle>
|
<DialogTitle>Update Reward</DialogTitle>
|
||||||
<WarningBox>
|
<WarningBox>
|
||||||
Existing customers with this coupon will not be affected
|
Existing customers with this coupon will not be affected
|
||||||
</WarningBox>
|
</WarningBox>
|
||||||
|
|
||||||
{selectedCoupon && (
|
{selectedReward && (
|
||||||
<CouponConfig coupon={selectedCoupon} setCoupon={setSelectedCoupon} />
|
<RewardConfig reward={selectedReward} setReward={setSelectedReward} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -77,4 +77,4 @@ function UpdateCoupon({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UpdateCoupon;
|
export default UpdateReward;
|
||||||
Reference in New Issue
Block a user