From bb435415bb1c99eb74bfc58b1cf3de7c19cffa08 Mon Sep 17 00:00:00 2001
From: John Yeo
Date: Wed, 9 Apr 2025 14:26:36 +0100
Subject: [PATCH] wrote tests for referral programs
---
.../src/services/products/CouponService.tsx | 4 +-
.../customers/customer/CustomerToolbar.tsx | 4 +-
.../add-coupon/AddCouponDialogContent.tsx | 14 +-
frontend/src/views/products/ProductsView.tsx | 4 +-
.../views/products/coupons/CouponConfig.tsx | 10 +-
.../products/coupons/CouponRowToolbar.tsx | 4 +-
.../views/products/coupons/CreateCoupon.tsx | 10 +-
server/src/external/autumn/autumnCli.ts | 52 ++++-
.../src/external/stripe/stripeCouponUtils.ts | 8 +-
.../external/stripe/stripeOnboardingUtils.ts | 1 +
.../src/external/stripe/stripeProductUtils.ts | 34 ++-
server/src/external/stripe/stripeWebhooks.ts | 2 +
.../handleCheckoutCompleted.ts | 5 +-
.../handleCusDiscountDeleted.ts | 94 +++++++++
.../webhookHandlers/handleInvoicePaid.ts | 3 +-
.../webhookHandlers/handleSubUpdated.ts | 11 -
server/src/internal/api/apiRouter.ts | 3 +-
.../internal/api/rewards/referralRouter.ts | 24 +++
.../src/internal/api/rewards/rewardRouter.ts | 12 +-
.../api/rewards/rewardTriggerRouter.ts | 3 +
server/src/internal/customers/CusService.ts | 9 +-
.../products/internalProductRouter.ts | 3 +-
server/src/internal/public/publicRouter.ts | 115 +++++------
.../rewards/RewardRedemptionService.ts | 14 ++
server/src/internal/rewards/RewardService.ts | 6 +-
server/src/internal/rewards/rewardUtils.ts | 6 +-
server/src/utils/genUtils.ts | 4 +
server/test.sh | 13 +-
server/tests/00_setup.ts | 8 +-
server/tests/advanced/coupons/coupon1.ts | 16 +-
server/tests/advanced/coupons/coupon2.ts | 16 +-
server/tests/advanced/usage/group_by.ts | 2 +-
server/tests/basic/referrals/referrals1.ts | 194 ++++++++++++++++++
server/tests/basic/referrals/referrals2.ts | 171 +++++++++++++++
server/tests/cli/AutumnCli.ts | 4 +-
server/tests/global.ts | 38 +++-
server/tests/utils/init.ts | 27 ++-
server/tests/utils/setup.ts | 69 ++++---
shared/models/rewardModels/rewardModels.ts | 9 +-
.../{CouponService.tsx => RewardService.tsx} | 14 +-
.../customers/customer/CustomerToolbar.tsx | 2 +-
.../customer/UpdateCustomerDialog.tsx | 4 +-
.../add-coupon/AddCouponDialogContent.tsx | 20 +-
vite/src/views/products/ProductsView.tsx | 24 +--
.../reward-triggers/CreateRewardTriger.tsx | 4 +-
.../reward-triggers/RewardTriggerConfig.tsx | 6 +-
.../reward-triggers/RewardTriggersTable.tsx | 2 +-
.../CreateReward.tsx} | 34 ++-
.../RewardConfig.tsx} | 98 +++++----
.../RewardRowToolbar.tsx} | 16 +-
.../RewardsTable.tsx} | 48 ++---
.../UpdateReward.tsx} | 34 +--
52 files changed, 974 insertions(+), 358 deletions(-)
create mode 100644 server/tests/basic/referrals/referrals1.ts
create mode 100644 server/tests/basic/referrals/referrals2.ts
rename vite/src/services/products/{CouponService.tsx => RewardService.tsx} (73%)
rename vite/src/views/products/{coupons/CreateCoupon.tsx => rewards/CreateReward.tsx} (73%)
rename vite/src/views/products/{coupons/CouponConfig.tsx => rewards/RewardConfig.tsx} (79%)
rename vite/src/views/products/{coupons/CouponRowToolbar.tsx => rewards/RewardRowToolbar.tsx} (86%)
rename vite/src/views/products/{coupons/CouponsTable.tsx => rewards/RewardsTable.tsx} (65%)
rename vite/src/views/products/{coupons/UpdateCoupon.tsx => rewards/UpdateReward.tsx} (68%)
diff --git a/frontend/src/services/products/CouponService.tsx b/frontend/src/services/products/CouponService.tsx
index ba313308a..3ab3786f2 100644
--- a/frontend/src/services/products/CouponService.tsx
+++ b/frontend/src/services/products/CouponService.tsx
@@ -1,4 +1,4 @@
-import { CreateCoupon } from "@autumn/shared";
+import { CreateReward } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { AxiosInstance } from "axios";
@@ -9,7 +9,7 @@ export class CouponService {
data,
}: {
axiosInstance: AxiosInstance;
- data: CreateCoupon;
+ data: CreateReward;
}) {
await axiosInstance.post("/v1/coupons", data);
}
diff --git a/frontend/src/views/customers/customer/CustomerToolbar.tsx b/frontend/src/views/customers/customer/CustomerToolbar.tsx
index ee18c3ca6..aee916dc1 100644
--- a/frontend/src/views/customers/customer/CustomerToolbar.tsx
+++ b/frontend/src/views/customers/customer/CustomerToolbar.tsx
@@ -12,7 +12,7 @@ import { useState } from "react";
import toast from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { useAxiosInstance } from "@/services/useAxiosInstance";
-import { Coupon, Customer } from "@autumn/shared";
+import { Reward, Customer } from "@autumn/shared";
import { useCustomerContext } from "./CustomerContext";
import { CusService } from "@/services/customers/CusService";
import { useRouter } from "next/navigation";
@@ -76,7 +76,7 @@ export const CustomerToolbar = ({
}}
>
-
Add Coupon
+
Add Reward
diff --git a/frontend/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx b/frontend/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx
index f249f2cb2..49a98fd0a 100644
--- a/frontend/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx
+++ b/frontend/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx
@@ -2,7 +2,7 @@ import { SelectContent } from "@/components/ui/select";
import { SelectValue } from "@/components/ui/select";
import { SelectTrigger } from "@/components/ui/select";
import { DialogFooter } from "@/components/ui/dialog";
-import { Coupon } from "@autumn/shared";
+import { Reward } from "@autumn/shared";
import { Select, SelectItem } from "@/components/ui/select";
import {
Dialog,
@@ -22,28 +22,28 @@ const AddCouponDialogContent = ({
setOpen: (open: boolean) => void;
}) => {
const { coupons } = useCustomerContext();
- const [couponSelected, setCouponSelected] = useState(null);
+ const [couponSelected, setCouponSelected] = useState(null);
const handleAddClicked = async () => {};
return (
- Add Coupon
+ Add Reward
diff --git a/frontend/src/views/products/ProductsView.tsx b/frontend/src/views/products/ProductsView.tsx
index b0ca5f913..605684174 100644
--- a/frontend/src/views/products/ProductsView.tsx
+++ b/frontend/src/views/products/ProductsView.tsx
@@ -17,7 +17,7 @@ import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTicketSimple } from "@fortawesome/pro-duotone-svg-icons";
import { CouponsTable } from "./coupons/CouponsTable";
-import CreateCoupon from "./coupons/CreateCoupon";
+import CreateReward from "./coupons/CreateReward";
function ProductsView({ env }: { env: AppEnv }) {
const [selectedProduct, setSelectedProduct] = useState(null);
@@ -83,7 +83,7 @@ function ProductsView({ env }: { env: AppEnv }) {
-
+
)}
diff --git a/frontend/src/views/products/coupons/CouponConfig.tsx b/frontend/src/views/products/coupons/CouponConfig.tsx
index 4fc343717..178db0b08 100644
--- a/frontend/src/views/products/coupons/CouponConfig.tsx
+++ b/frontend/src/views/products/coupons/CouponConfig.tsx
@@ -8,7 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
import {
CouponDurationType,
- CreateCoupon,
+ CreateReward,
DiscountType,
Feature,
} from "@autumn/shared";
@@ -37,8 +37,8 @@ export const CouponConfig = ({
coupon,
setCoupon,
}: {
- coupon: CreateCoupon;
- setCoupon: (coupon: CreateCoupon) => void;
+ coupon: CreateReward;
+ setCoupon: (coupon: CreateReward) => void;
}) => {
const { org } = useProductsContext();
return (
@@ -172,8 +172,8 @@ const ProductPriceSelector = ({
coupon,
setCoupon,
}: {
- coupon: CreateCoupon;
- setCoupon: (coupon: CreateCoupon) => void;
+ coupon: CreateReward;
+ setCoupon: (coupon: CreateReward) => void;
}) => {
const { products, features } = useProductsContext();
const [open, setOpen] = useState(false);
diff --git a/frontend/src/views/products/coupons/CouponRowToolbar.tsx b/frontend/src/views/products/coupons/CouponRowToolbar.tsx
index 887e5ee3a..a18e12390 100644
--- a/frontend/src/views/products/coupons/CouponRowToolbar.tsx
+++ b/frontend/src/views/products/coupons/CouponRowToolbar.tsx
@@ -13,7 +13,7 @@ import toast from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { useAxiosInstance } from "@/services/useAxiosInstance";
-import { Coupon, Feature } from "@autumn/shared";
+import { Reward, Feature } from "@autumn/shared";
import { FeatureService } from "@/services/FeatureService";
import { getBackendErr } from "@/utils/genUtils";
import { useProductsContext } from "../ProductsContext";
@@ -24,7 +24,7 @@ export const CouponRowToolbar = ({
coupon,
}: {
className?: string;
- coupon: Coupon;
+ coupon: Reward;
}) => {
const { env, mutate } = useProductsContext();
const axiosInstance = useAxiosInstance({ env });
diff --git a/frontend/src/views/products/coupons/CreateCoupon.tsx b/frontend/src/views/products/coupons/CreateCoupon.tsx
index 29bad52ad..64c9bf2a5 100644
--- a/frontend/src/views/products/coupons/CreateCoupon.tsx
+++ b/frontend/src/views/products/coupons/CreateCoupon.tsx
@@ -20,7 +20,7 @@ import toast from "react-hot-toast";
import { PlusIcon } from "lucide-react";
import {
CouponDurationType,
- CreateCoupon as CreateCouponType,
+ CreateReward as CreateCouponType,
DiscountType,
} from "@autumn/shared";
import { getBackendErr } from "@/utils/genUtils";
@@ -41,7 +41,7 @@ const defaultCoupon: CreateCouponType = {
apply_to_all: true,
};
-function CreateCoupon() {
+function CreateReward() {
const { mutate, env } = useProductsContext();
const axiosInstance = useAxiosInstance({ env: env });
@@ -80,12 +80,12 @@ function CreateCoupon() {
className="w-full"
startIcon={}
>
- Create Coupon
+ Create Reward
- Create Coupon
+ Create Reward
{/* {
+ 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 () => {
await this.post(`/products/all/init_stripe`, {});
};
diff --git a/server/src/external/stripe/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils.ts
index 2ceb58578..4d60cdec7 100644
--- a/server/src/external/stripe/stripeCouponUtils.ts
+++ b/server/src/external/stripe/stripeCouponUtils.ts
@@ -1,6 +1,6 @@
import RecaseError from "@/utils/errorUtils.js";
import {
- Coupon,
+ Reward,
CouponDurationType,
DiscountType,
ErrCode,
@@ -14,7 +14,7 @@ import {
import { logger } from "@trigger.dev/sdk/v3";
import { Stripe } from "stripe";
-const couponToStripeDuration = (coupon: Coupon) => {
+const couponToStripeDuration = (coupon: Reward) => {
if (
coupon.duration_type === CouponDurationType.OneOff &&
coupon.should_rollover
@@ -45,7 +45,7 @@ const couponToStripeValue = ({
coupon,
org,
}: {
- coupon: Coupon;
+ coupon: Reward;
org: Organization;
}) => {
if (coupon.discount_type === DiscountType.Percentage) {
@@ -66,7 +66,7 @@ export const createStripeCoupon = async ({
org,
prices,
}: {
- coupon: Coupon;
+ coupon: Reward;
stripeCli: Stripe;
org: Organization;
prices: (Price & { product: Product })[];
diff --git a/server/src/external/stripe/stripeOnboardingUtils.ts b/server/src/external/stripe/stripeOnboardingUtils.ts
index 164d8d2d1..5dee5e876 100644
--- a/server/src/external/stripe/stripeOnboardingUtils.ts
+++ b/server/src/external/stripe/stripeOnboardingUtils.ts
@@ -30,6 +30,7 @@ export const createWebhookEndpoint = async (
"invoice.created",
"invoice.finalized",
"subscription_schedule.canceled",
+ "customer.discount.deleted",
],
});
diff --git a/server/src/external/stripe/stripeProductUtils.ts b/server/src/external/stripe/stripeProductUtils.ts
index 6b659b488..7b21738de 100644
--- a/server/src/external/stripe/stripeProductUtils.ts
+++ b/server/src/external/stripe/stripeProductUtils.ts
@@ -66,13 +66,35 @@ export const deactivateStripeMeters = async ({
}) => {
const stripeCli = createStripeCli({ org, env });
- const stripeMeters = await stripeCli.billing.meters.list({
- limit: 100,
- status: "active",
- });
+ let allStripeMeters = [];
+ let hasMore = true;
+ let startingAfter;
- for (const meter of stripeMeters.data) {
- await stripeCli.billing.meters.deactivate(meter.id);
+ while (hasMore) {
+ 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));
}
};
diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts
index 84d67a5df..8840e0afd 100644
--- a/server/src/external/stripe/stripeWebhooks.ts
+++ b/server/src/external/stripe/stripeWebhooks.ts
@@ -167,7 +167,9 @@ stripeWebhookRouter.post(
discount: event.data.object,
env,
logger,
+ res: response,
});
+ return;
break;
}
} catch (error) {
diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts
index e5dfa1711..667b741ca 100644
--- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts
@@ -1,5 +1,5 @@
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 { CusProductService } from "@/internal/customers/products/CusProductService.js";
import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js";
@@ -24,9 +24,6 @@ import {
attachToInsertParams,
getPricesForProduct,
} 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 { createStripeSub } from "../stripeSubUtils/createStripeSub.js";
import { getAlignedIntervalUnix } from "@/internal/prices/billingIntervalUtils.js";
diff --git a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
index f73b8511a..bd6ef7ac0 100644
--- a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
@@ -1,6 +1,79 @@
import { CusService } from "@/internal/customers/CusService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.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({
sb,
@@ -8,12 +81,14 @@ export async function handleCusDiscountDeleted({
discount,
env,
logger,
+ res,
}: {
sb: any;
org: any;
discount: any;
env: any;
logger: any;
+ res: any;
}) {
let customer = await CusService.getByStripeId({
sb,
@@ -52,6 +127,25 @@ export async function handleCusDiscountDeleted({
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, {
coupon: reward.internal_id,
});
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts
index b76486423..0b98148a5 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts
@@ -260,6 +260,7 @@ const handleInvoicePaidDiscount = async ({
}
const curCoupon = discount.coupon;
+
if (!curCoupon) {
continue;
}
@@ -308,8 +309,6 @@ const handleInvoicePaidDiscount = async ({
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({
id: `${couponId}_${generateId("roll")}`,
name: discount.coupon.name as string,
diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts
index 0635ad33e..757559d41 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts
@@ -3,17 +3,6 @@ import { AppEnv, CusProductStatus, Organization } from "@autumn/shared";
import Stripe from "stripe";
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 ({
sb,
org,
diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts
index 3506133e5..956acdf82 100644
--- a/server/src/internal/api/apiRouter.ts
+++ b/server/src/internal/api/apiRouter.ts
@@ -16,7 +16,7 @@ import { entityRouter } from "./entities/entityRouter.js";
import { migrationRouter } from "./migrations/migrationRouter.js";
import rewardRouter from "./rewards/rewardRouter.js";
import { rewardTriggerRouter } from "./rewards/rewardTriggerRouter.js";
-import { referralRouter } from "./rewards/referralRouter.js";
+import { redemptionRouter, referralRouter } from "./rewards/referralRouter.js";
const apiRouter = Router();
@@ -73,5 +73,6 @@ apiRouter.use("/migrations", migrationRouter);
// REWARDS
apiRouter.use("/reward-triggers", rewardTriggerRouter);
apiRouter.use("/referrals", referralRouter);
+apiRouter.use("/redemptions", redemptionRouter);
export { apiRouter };
diff --git a/server/src/internal/api/rewards/referralRouter.ts b/server/src/internal/api/rewards/referralRouter.ts
index fe81d778a..b883d3f46 100644
--- a/server/src/internal/api/rewards/referralRouter.ts
+++ b/server/src/internal/api/rewards/referralRouter.ts
@@ -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);
+ },
+ })
+);
diff --git a/server/src/internal/api/rewards/rewardRouter.ts b/server/src/internal/api/rewards/rewardRouter.ts
index 3fe469917..1c9c296bf 100644
--- a/server/src/internal/api/rewards/rewardRouter.ts
+++ b/server/src/internal/api/rewards/rewardRouter.ts
@@ -1,5 +1,5 @@
import express from "express";
-import { CouponDurationType, CreateCouponSchema } from "@autumn/shared";
+import { CreateRewardSchema } from "@autumn/shared";
import { handleRequestError } from "@/utils/errorUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -18,7 +18,7 @@ rewardRouter.post("", async (req: any, res: any) => {
const { orgId, env } = req;
const couponBody = req.body;
- const couponData = CreateCouponSchema.parse(couponBody);
+ const couponData = CreateRewardSchema.parse(couponBody);
const org = await OrgService.getFromReq(req);
const newCoupon = initCoupon({
coupon: couponData,
@@ -74,12 +74,12 @@ rewardRouter.post("", async (req: any, res: any) => {
prices,
});
- console.log("✅ Coupon successfully created in Stripe");
+ console.log("✅ Reward successfully created in Stripe");
const insertedCoupon = await RewardService.insert({
sb: req.sb,
data: newCoupon,
});
- console.log("✅ Coupon successfully inserted into db");
+ console.log("✅ Reward successfully inserted into db");
res.status(200).json(insertedCoupon);
} catch (error) {
@@ -117,7 +117,7 @@ rewardRouter.delete("/:id", async (req: any, res: any) => {
res.status(200).json({
success: true,
- message: "Coupon deleted successfully",
+ message: "Reward deleted successfully",
});
} catch (error) {
handleRequestError({
@@ -135,7 +135,7 @@ rewardRouter.post("/:id", async (req: any, res: any) => {
const { orgId, env } = req;
const couponBody = req.body;
- console.log("Coupon body", couponBody);
+ console.log("Reward body", couponBody);
const org = await OrgService.getFromReq(req);
const stripeCli = createStripeCli({
org,
diff --git a/server/src/internal/api/rewards/rewardTriggerRouter.ts b/server/src/internal/api/rewards/rewardTriggerRouter.ts
index 2097823ac..55c6d7bcb 100644
--- a/server/src/internal/api/rewards/rewardTriggerRouter.ts
+++ b/server/src/internal/api/rewards/rewardTriggerRouter.ts
@@ -26,6 +26,9 @@ rewardTriggerRouter.post("", (req, res) =>
}
);
+ console.log("✅ Successfully created reward trigger");
+ console.log(createdRewardTrigger);
+
return res.status(200).json(createdRewardTrigger);
},
})
diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts
index 244ce5f7a..c6bb8164c 100644
--- a/server/src/internal/customers/CusService.ts
+++ b/server/src/internal/customers/CusService.ts
@@ -281,14 +281,17 @@ export class CusService {
const { data, error } = await sb
.from("customers")
.select()
- .eq("processor->>id", stripeId)
- .single();
+ .eq("processor->>id", stripeId);
if (error) {
throw error;
}
- return data;
+ if (data.length === 0) {
+ return null;
+ }
+
+ return data[0];
}
//search customers
diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts
index f6815bf00..375f5c904 100644
--- a/server/src/internal/products/internalProductRouter.ts
+++ b/server/src/internal/products/internalProductRouter.ts
@@ -52,7 +52,8 @@ productRouter.get("/data", async (req: any, res) => {
live_pkey: org.live_pkey,
default_currency: org.default_currency,
},
- coupons,
+ // coupons,
+ rewards: coupons,
rewardTriggers,
});
} catch (error) {
diff --git a/server/src/internal/public/publicRouter.ts b/server/src/internal/public/publicRouter.ts
index 30838d8c1..e4e10ab95 100644
--- a/server/src/internal/public/publicRouter.ts
+++ b/server/src/internal/public/publicRouter.ts
@@ -81,7 +81,6 @@ const publicRouterMiddleware = async (req: any, res: any, next: any) => {
publicRouter.use(publicRouterMiddleware);
publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
-
try {
const customerId = req.params.customer_id;
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,
sb: req.sb,
orgId: req.org.id,
@@ -112,13 +111,7 @@ publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
logger: req.logtail,
});
- res.status(200).json({
- customer: CustomerResponseSchema.parse(customer),
- products: main,
- add_ons: addOns,
- entitlements: balances,
- invoices,
- });
+ res.status(200).json(cusData);
} catch (error) {
handleRequestError({ req, error, res, action: "get customer" });
}
@@ -130,63 +123,59 @@ publicRouter.get(
try {
const customerId = req.params.customerId;
- const customer = await CusService.getById({
- sb: req.sb,
- id: customerId,
- orgId: req.org.id,
- env: req.env,
- 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: [],
+ const customer = await CusService.getById({
+ sb: req.sb,
+ id: customerId,
+ orgId: req.org.id,
+ env: req.env,
+ logger: req.logtail,
});
- if (processed.status == CusProductStatus.Trialing) {
- processed.status = CusProductStatus.Active;
+ if (!customer) {
+ return res.status(404).json({
+ message: `Customer ${customerId} not found`,
+ });
}
- let isAddOn = cusProduct.product.is_add_on;
- if (isAddOn) {
- addOns.push(processed);
- } else {
- main.push(processed);
+ 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 = [];
- res.status(200).json({
- main,
+ for (const cusProduct of cusProducts) {
+ 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,
});
} catch (error) {
@@ -205,17 +194,17 @@ publicRouter.get(
orgId: req.org.id,
env: req.env,
});
-
+
const features = await FeatureService.getFeatures({
sb: req.sb,
orgId: req.org.id,
env: req.env,
});
-
+
const prices = product.prices;
-
+
const options = getOptionsFromPrices(prices, features);
-
+
res.status(200).json(options);
} catch (error) {
handleRequestError({ req, error, res, action: "get product options" });
diff --git a/server/src/internal/rewards/RewardRedemptionService.ts b/server/src/internal/rewards/RewardRedemptionService.ts
index 6f200f1fe..059cc4187 100644
--- a/server/src/internal/rewards/RewardRedemptionService.ts
+++ b/server/src/internal/rewards/RewardRedemptionService.ts
@@ -2,6 +2,20 @@ import { notNullish } from "@/utils/genUtils.js";
import { RewardRedemption, RewardTriggerEvent } from "@autumn/shared";
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({
sb,
internalCustomerId,
diff --git a/server/src/internal/rewards/RewardService.ts b/server/src/internal/rewards/RewardService.ts
index 18eb8ac19..8abcaaa0c 100644
--- a/server/src/internal/rewards/RewardService.ts
+++ b/server/src/internal/rewards/RewardService.ts
@@ -1,5 +1,5 @@
import { generateId } from "@/utils/genUtils.js";
-import { AppEnv, Coupon } from "@autumn/shared";
+import { AppEnv, Reward } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
export class RewardService {
@@ -8,7 +8,7 @@ export class RewardService {
data,
}: {
sb: SupabaseClient;
- data: Coupon | Coupon[];
+ data: Reward | Reward[];
}) {
const { data: insertedData, error } = await sb
.from("rewards")
@@ -103,7 +103,7 @@ export class RewardService {
internalId: string;
env: AppEnv;
orgId: string;
- update: Partial;
+ update: Partial;
}) {
const { data, error } = await sb
.from("rewards")
diff --git a/server/src/internal/rewards/rewardUtils.ts b/server/src/internal/rewards/rewardUtils.ts
index 1010a4756..4de17f42e 100644
--- a/server/src/internal/rewards/rewardUtils.ts
+++ b/server/src/internal/rewards/rewardUtils.ts
@@ -1,5 +1,5 @@
import { generateId } from "@/utils/genUtils.js";
-import { Coupon, CreateCoupon } from "@autumn/shared";
+import { Reward, CreateReward } from "@autumn/shared";
export const initCoupon = ({
coupon,
@@ -7,7 +7,7 @@ export const initCoupon = ({
env,
id,
}: {
- coupon: CreateCoupon;
+ coupon: CreateReward;
orgId: string;
env: string;
id?: string;
@@ -31,7 +31,7 @@ export enum CouponType {
Standard = "standard",
}
-export const getCouponType = (coupon: Coupon) => {
+export const getCouponType = (coupon: Reward) => {
if (!coupon) return null;
if (coupon.apply_to_all && coupon.should_rollover) {
return CouponType.AddInvoiceBalance;
diff --git a/server/src/utils/genUtils.ts b/server/src/utils/genUtils.ts
index 5afde6f94..28b063ba5 100644
--- a/server/src/utils/genUtils.ts
+++ b/server/src/utils/genUtils.ts
@@ -43,3 +43,7 @@ export const notNullish = (value: any) => {
export const formatUnixToDateTime = (unixDate: number) => {
return format(new Date(unixDate), "yyyy MMM dd HH:mm:ss");
};
+
+export const timeout = (ms: number) => {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+};
diff --git a/server/test.sh b/server/test.sh
index 4229b137f..f9c784d04 100755
--- a/server/test.sh
+++ b/server/test.sh
@@ -5,16 +5,17 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts"
# TEST PARALLEL
if [ "$1" == "basic-parallel" ]; then
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \
- tests/basic/*.ts \
- tests/basic/entities/*.ts \
- # tests/attach/**/*.ts \
+ tests/basic/referrals/*.ts \
+ tests/attach/**/*.ts \
+ # tests/basic/*.ts \
+ # tests/basic/entities/*.ts \
elif [ "$1" == "advanced-parallel" ]; then
MOCHA_PARALLEL=true \
$MOCHA_SETUP \
- && $MOCHA_CMD 'tests/advanced/usage/*.ts' \
- # && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\
- # && $MOCHA_CMD 'tests/advanced/coupons/*.ts' \
+ && $MOCHA_CMD 'tests/advanced/coupons/*.ts' \
+ && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\
+ # && $MOCHA_CMD 'tests/advanced/usage/*.ts' \
elif [ "$1" == "alex-parallel" ]; then
diff --git a/server/tests/00_setup.ts b/server/tests/00_setup.ts
index ed07de119..bedfa1a4b 100644
--- a/server/tests/00_setup.ts
+++ b/server/tests/00_setup.ts
@@ -7,9 +7,10 @@ import {
creditSystems,
advanceProducts,
attachProducts,
- coupons,
+ rewards,
oneTimeProducts,
entityProducts,
+ referralPrograms,
} from "./global.js";
const ORG_SLUG = "unit-test-org";
@@ -32,9 +33,10 @@ describe("Initialize org for tests", () => {
...oneTimeProducts,
...entityProducts,
} as any,
- coupons: { ...coupons } as any,
+ rewards: { ...rewards } as any,
+ rewardTriggers: { ...referralPrograms } as any,
});
console.log("--------------------------------");
});
-});
\ No newline at end of file
+});
diff --git a/server/tests/advanced/coupons/coupon1.ts b/server/tests/advanced/coupons/coupon1.ts
index 4dd8ee3c7..4914d6ddd 100644
--- a/server/tests/advanced/coupons/coupon1.ts
+++ b/server/tests/advanced/coupons/coupon1.ts
@@ -1,6 +1,6 @@
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.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 { Customer } from "@autumn/shared";
import { expect } from "chai";
@@ -8,7 +8,7 @@ import chalk from "chalk";
import { addDays, addHours, addMonths, format } from "date-fns";
import Stripe from "stripe";
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 { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js";
@@ -29,7 +29,7 @@ describe(
let customer: Customer;
let testClockId: string;
- let couponAmount = coupons.rolloverAll.discount_value;
+ let couponAmount = rewards.rolloverAll.discount_value;
before(async function () {
const { testClockId: testClockId1, customer: customer1 } =
@@ -65,7 +65,7 @@ describe(
await completeCheckoutForm(
res.checkout_url,
undefined,
- coupons.rolloverAll.id
+ rewards.rolloverAll.id
);
await timeout(20000);
@@ -88,7 +88,7 @@ describe(
try {
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- coupons.rolloverAll.id
+ rewards.rolloverAll.id
);
// Expect amount to be original amount - pro price
@@ -97,7 +97,7 @@ describe(
console.error("--------------------------------");
console.error(
"Expected stripe cus to have coupon",
- coupons.rolloverAll
+ rewards.rolloverAll
);
console.error("Actual stripe cus discount", cusDiscount);
throw error;
@@ -140,13 +140,13 @@ describe(
try {
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- coupons.rolloverAll.id
+ rewards.rolloverAll.id
);
expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
} catch (error) {
console.log("--------------------------------");
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);
throw error;
}
diff --git a/server/tests/advanced/coupons/coupon2.ts b/server/tests/advanced/coupons/coupon2.ts
index 25c23c81c..8d9e90be6 100644
--- a/server/tests/advanced/coupons/coupon2.ts
+++ b/server/tests/advanced/coupons/coupon2.ts
@@ -1,13 +1,13 @@
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.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 { Customer } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import Stripe from "stripe";
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 { getFixedPriceAmount, timeout } from "tests/utils/genUtils.js";
import {
@@ -27,7 +27,7 @@ describe(
let customer: Customer;
let testClockId: string;
- let couponAmount = coupons.rolloverAll.discount_value;
+ let couponAmount = rewards.rolloverUsage.discount_value;
before(async function () {
const { testClockId: testClockId1, customer: customer1 } =
@@ -61,7 +61,7 @@ describe(
await completeCheckoutForm(
res.checkout_url,
undefined,
- coupons.rolloverUsage.id
+ rewards.rolloverUsage.id
);
await timeout(10000);
@@ -85,7 +85,7 @@ describe(
try {
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- coupons.rolloverUsage.id
+ rewards.rolloverUsage.id
);
// Expect amount to be original amount - pro price
@@ -94,7 +94,7 @@ describe(
logger.error("--------------------------------");
logger.error(
"Expected stripe cus to have coupon",
- coupons.rolloverUsage
+ rewards.rolloverUsage
);
logger.error("Actual stripe cus discount", cusDiscount);
throw error;
@@ -139,7 +139,7 @@ describe(
try {
expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- coupons.rolloverUsage.id
+ rewards.rolloverUsage.id
);
expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
} catch (error) {
@@ -147,7 +147,7 @@ describe(
logger.error("coupon2, cycle 1 failed");
logger.error(
"Expected stripe cus to have coupon",
- coupons.rolloverUsage
+ rewards.rolloverUsage
);
logger.error("Actual stripe cus discount", cusDiscount);
throw error;
diff --git a/server/tests/advanced/usage/group_by.ts b/server/tests/advanced/usage/group_by.ts
index 4983bc1d9..fcbd10863 100644
--- a/server/tests/advanced/usage/group_by.ts
+++ b/server/tests/advanced/usage/group_by.ts
@@ -14,7 +14,7 @@ import { sendGPUEvents } from "../../utils/advancedUsageUtils.js";
import chalk from "chalk";
const PRECISION = 12;
-describe(`${chalk.yellowBright(
+describe.skip(`${chalk.yellowBright(
"Testing group by -- regular metered1 feature"
)}`, () => {
let customerId = "group-by-basic-metered";
diff --git a/server/tests/basic/referrals/referrals1.ts b/server/tests/basic/referrals/referrals1.ts
new file mode 100644
index 000000000..7285c5ff9
--- /dev/null
+++ b/server/tests/basic/referrals/referrals1.ts
@@ -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);
+ });
+});
diff --git a/server/tests/basic/referrals/referrals2.ts b/server/tests/basic/referrals/referrals2.ts
new file mode 100644
index 000000000..e1430c90d
--- /dev/null
+++ b/server/tests/basic/referrals/referrals2.ts
@@ -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);
+ });
+});
diff --git a/server/tests/cli/AutumnCli.ts b/server/tests/cli/AutumnCli.ts
index 68047b4a8..fc77a3f03 100644
--- a/server/tests/cli/AutumnCli.ts
+++ b/server/tests/cli/AutumnCli.ts
@@ -1,7 +1,7 @@
import { el } from "date-fns/locale";
import { getAxiosInstance } from "../utils/setup.js";
import RecaseError from "@/utils/errorUtils.js";
-import { AppEnv, CreateCoupon } from "@autumn/shared";
+import { AppEnv, CreateReward } from "@autumn/shared";
const handleAxiosError = (error: any) => {
if (error.response.data) {
// console.log(error.response.data);
@@ -212,7 +212,7 @@ export class AutumnCli {
return data;
}
- static async createCoupon(coupon: CreateCoupon) {
+ static async createCoupon(coupon: CreateReward) {
const axiosInstance = getAxiosInstance();
const { data } = await axiosInstance.post(`/v1/coupons`, coupon);
return data;
diff --git a/server/tests/global.ts b/server/tests/global.ts
index 21b3e3031..233261ef5 100644
--- a/server/tests/global.ts
+++ b/server/tests/global.ts
@@ -5,17 +5,21 @@ import {
AllowanceType,
AppEnv,
BillingInterval,
+ CouponDurationType,
+ DiscountType,
EntInterval,
Feature,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { FeatureType } from "@autumn/shared";
import {
- initCoupon,
+ initReward,
initEntitlement,
initFeature,
initFreeTrial,
initPrice,
initProduct,
+ initRewardTrigger,
} from "./utils/init.js";
import { createSupabaseClient } from "@/external/supabaseUtils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -762,27 +766,49 @@ export const entityProducts = {
oneTier: true,
billingUnits: 1,
// Carry over usage
- })
+ }),
],
freeTrial: null,
}),
-
};
-export const coupons = {
- rolloverAll: initCoupon({
+export const rewards = {
+ rolloverAll: initReward({
id: "rolloverAll",
discountValue: 1000,
rollover: true,
applyToAll: true,
}),
- rolloverUsage: initCoupon({
+ rolloverUsage: initReward({
id: "rolloverUsage",
discountValue: 1000,
rollover: true,
onlyUsagePrices: true,
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";
diff --git a/server/tests/utils/init.ts b/server/tests/utils/init.ts
index 8cd8fcc57..b8e955234 100644
--- a/server/tests/utils/init.ts
+++ b/server/tests/utils/init.ts
@@ -14,6 +14,7 @@ import {
FreeTrial,
Organization,
PriceType,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { getAxiosInstance } from "./setup.js";
import { SupabaseClient } from "@supabase/supabase-js";
@@ -326,8 +327,8 @@ export const initCustomer = async ({
}
};
-// Init Coupon
-export const initCoupon = ({
+// Init Reward
+export const initReward = ({
id,
discountType = DiscountType.Fixed,
discountValue,
@@ -361,3 +362,25 @@ export const initCoupon = ({
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,
+ };
+};
diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts
index 93a75822e..b2c16dc2a 100644
--- a/server/tests/utils/setup.ts
+++ b/server/tests/utils/setup.ts
@@ -1,12 +1,14 @@
import { createSupabaseClient } from "@/external/supabaseUtils.js";
import {
AppEnv,
- CreateCoupon,
+ CreateReward,
Feature,
FeatureType,
FullProduct,
Price,
PriceType,
+ Reward,
+ RewardTrigger,
} from "@autumn/shared";
import axios from "axios";
@@ -18,6 +20,8 @@ import {
} from "./stripeUtils.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import Stripe from "stripe";
+import { Autumn } from "@/external/autumn/autumnCli.js";
+import { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js";
export const getAxiosInstance = (
apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!
@@ -168,11 +172,15 @@ export const clearOrg = async ({
await deleteAllStripeTestClocks({ stripeCli });
console.log(" ✅ Deleted Stripe test clocks");
+ // Delete all stripe meters
+ await deactivateStripeMeters({ org, env });
+ console.log(" ✅ Deactivated Stripe meters");
+
// Batch delete coupons
const batchDeleteCoupons = [];
const { data: coupons, error: couponError } = await sb
- .from("coupons")
+ .from("rewards")
.delete()
.eq("org_id", orgId)
.eq("env", env)
@@ -207,16 +215,19 @@ export const setupOrg = async ({
env,
features,
products,
- coupons,
+ rewards,
+ rewardTriggers,
}: {
orgId: string;
env: AppEnv;
features: Record;
products: Record;
- coupons: Record;
+ rewards: Record;
+ rewardTriggers: Record;
}) => {
const axiosInstance = getAxiosInstance();
const sb = createSupabaseClient();
+ const autumn = new Autumn(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!);
let insertFeatures = [];
for (const feature of Object.values(features)) {
@@ -298,14 +309,14 @@ export const setupOrg = async ({
// Insert coupons
let insertCoupons = [];
- for (const coupon of Object.values(coupons)) {
+ for (const reward of Object.values(rewards)) {
const createCoupon = async () => {
let priceIds = [];
- if (coupon.only_usage_prices) {
+ if (reward.only_usage_prices) {
let filteredProducts = allProducts.filter((product: FullProduct) => {
- if (coupon.product_ids) {
- return coupon.product_ids.includes(product.id);
+ if (reward.product_ids) {
+ return reward.product_ids.includes(product.id);
} else return true;
});
@@ -316,44 +327,56 @@ export const setupOrg = async ({
return price.id;
})
);
- } else if (coupon.product_ids) {
+ } else if (reward.product_ids) {
priceIds = allProducts
.filter((product: FullProduct) =>
- coupon.product_ids.includes(product.id)
+ reward.product_ids.includes(product.id)
)
.flatMap((product: FullProduct) =>
product.prices.map((price) => price.id)
);
}
- const newCoupon: CreateCoupon & { id: string } = {
- id: coupon.id,
- name: coupon.name,
+ const newReward: CreateReward & { id: string } = {
+ id: reward.id,
+ name: reward.name,
price_ids: priceIds,
promo_codes: [
{
- code: coupon.id,
+ code: reward.id,
},
],
- discount_type: coupon.discount_type,
- discount_value: coupon.discount_value,
- duration_type: coupon.duration_type,
- duration_value: coupon.duration_value,
- should_rollover: coupon.should_rollover,
- apply_to_all: coupon.apply_to_all,
+ discount_type: reward.discount_type,
+ discount_value: reward.discount_value,
+ duration_type: reward.duration_type,
+ duration_value: reward.duration_value,
+ should_rollover: reward.should_rollover,
+ apply_to_all: reward.apply_to_all,
};
- let couponRes = await AutumnCli.createCoupon(newCoupon);
+ let rewardRes = await autumn.rewards.create(newReward);
return {
- id: coupon.id,
- couponRes,
+ id: reward.id,
+ rewardRes,
};
};
insertCoupons.push(createCoupon());
}
+
await Promise.all(insertCoupons);
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
// How to check if mocha is in parallel mode?
if (process.env.MOCHA_PARALLEL) {
diff --git a/shared/models/rewardModels/rewardModels.ts b/shared/models/rewardModels/rewardModels.ts
index 06d41df56..73b3635fc 100644
--- a/shared/models/rewardModels/rewardModels.ts
+++ b/shared/models/rewardModels/rewardModels.ts
@@ -11,7 +11,7 @@ export enum DiscountType {
Fixed = "fixed",
}
-const CouponSchema = z.object({
+const RewardSchema = z.object({
internal_id: z.string(),
name: z.string().nullish(),
price_ids: z.array(z.string()),
@@ -31,13 +31,12 @@ const CouponSchema = z.object({
created_at: z.number(),
});
-export const CreateCouponSchema = CouponSchema.omit({
+export const CreateRewardSchema = RewardSchema.omit({
internal_id: true,
org_id: true,
env: true,
created_at: true,
});
-export type Coupon = z.infer;
-export type CreateCoupon = z.infer;
-export type Reward = z.infer;
+export type CreateReward = z.infer;
+export type Reward = z.infer;
diff --git a/vite/src/services/products/CouponService.tsx b/vite/src/services/products/RewardService.tsx
similarity index 73%
rename from vite/src/services/products/CouponService.tsx
rename to vite/src/services/products/RewardService.tsx
index 4b749181e..589dfabe7 100644
--- a/vite/src/services/products/CouponService.tsx
+++ b/vite/src/services/products/RewardService.tsx
@@ -1,20 +1,20 @@
-import { Coupon, CreateCoupon } from "@autumn/shared";
+import { Reward, CreateReward } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { AxiosInstance } from "axios";
-export class CouponService {
- static async createCoupon({
+export class RewardService {
+ static async createReward({
axiosInstance,
data,
}: {
axiosInstance: AxiosInstance;
- data: CreateCoupon;
+ data: CreateReward;
}) {
await axiosInstance.post("/v1/rewards", data);
}
- static async deleteCoupon({
+ static async deleteReward({
axiosInstance,
internalId,
}: {
@@ -24,14 +24,14 @@ export class CouponService {
await axiosInstance.delete(`/v1/rewards/${internalId}`);
}
- static async updateCoupon({
+ static async updateReward({
axiosInstance,
internalId,
data,
}: {
axiosInstance: AxiosInstance;
internalId: string;
- data: Coupon;
+ data: Reward;
}) {
await axiosInstance.post(`/v1/rewards/${internalId}`, data);
}
diff --git a/vite/src/views/customers/customer/CustomerToolbar.tsx b/vite/src/views/customers/customer/CustomerToolbar.tsx
index 69bb1f52e..a557b5958 100644
--- a/vite/src/views/customers/customer/CustomerToolbar.tsx
+++ b/vite/src/views/customers/customer/CustomerToolbar.tsx
@@ -99,7 +99,7 @@ export const CustomerToolbar = ({
}}
>
-
Add Coupon
+
Add Reward
diff --git a/vite/src/views/customers/customer/UpdateCustomerDialog.tsx b/vite/src/views/customers/customer/UpdateCustomerDialog.tsx
index 680adcff9..620b21b07 100644
--- a/vite/src/views/customers/customer/UpdateCustomerDialog.tsx
+++ b/vite/src/views/customers/customer/UpdateCustomerDialog.tsx
@@ -2,7 +2,7 @@ import { DialogFooter } from "@/components/ui/dialog";
import { getOriginalCouponId } from "@/utils/product/couponUtils";
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 { toast } from "sonner";
import { DialogTitle } from "@/components/ui/dialog";
@@ -29,7 +29,7 @@ const UpdateCustomerDialog = ({
setOpen: (open: boolean) => void;
}) => {
const { cusMutate } = useCustomerContext();
- const [couponSelected, setCouponSelected] = useState(null);
+ const [couponSelected, setCouponSelected] = useState(null);
const [customer, setCustomer] = useState(selectedCustomer);
const [loading, setLoading] = useState(false);
const env = useEnv();
diff --git a/vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx b/vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx
index fff6938bb..35389e965 100644
--- a/vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx
+++ b/vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx
@@ -2,7 +2,7 @@ import { SelectContent } from "@/components/ui/select";
import { SelectValue } from "@/components/ui/select";
import { SelectTrigger } from "@/components/ui/select";
import { DialogFooter } from "@/components/ui/dialog";
-import { Coupon } from "@autumn/shared";
+import { Reward } from "@autumn/shared";
import { Select, SelectItem } from "@/components/ui/select";
import { DialogContent, DialogTitle } from "@/components/ui/dialog";
import { useState } from "react";
@@ -22,7 +22,7 @@ const AddCouponDialogContent = ({
setOpen: (open: boolean) => void;
}) => {
const { cusMutate, customer, coupons, discount } = useCustomerContext();
- const [couponSelected, setCouponSelected] = useState(null);
+ const [couponSelected, setCouponSelected] = useState(null);
const [loading, setLoading] = useState(false);
const env = useEnv();
const axiosInstance = useAxiosInstance({ env });
@@ -38,7 +38,7 @@ const AddCouponDialogContent = ({
setOpen(false);
await cusMutate();
- toast.success("Coupon added to customer");
+ toast.success("Reward added to customer");
} catch (error) {
toast.error(getBackendErr(error, "Failed to create coupon"));
} finally {
@@ -51,7 +51,7 @@ const AddCouponDialogContent = ({
const getExistingCoupon = () => {
if (discount) {
return coupons.find(
- (c: Coupon) => c.internal_id === getOriginalCouponId(discount.coupon.id)
+ (c: Reward) => c.internal_id === getOriginalCouponId(discount.coupon.id)
);
} else {
return null;
@@ -60,10 +60,10 @@ const AddCouponDialogContent = ({
return (
- Add Coupon
+ Add Reward
{getExistingCoupon() && (
- Coupon {getExistingCoupon()?.name} already applied. Adding a new one
+ Reward {getExistingCoupon()?.name} already applied. Adding a new one
will replace the existing one.
)}
@@ -71,20 +71,20 @@ const AddCouponDialogContent = ({
diff --git a/vite/src/views/products/ProductsView.tsx b/vite/src/views/products/ProductsView.tsx
index 719224ece..fe30e91ad 100644
--- a/vite/src/views/products/ProductsView.tsx
+++ b/vite/src/views/products/ProductsView.tsx
@@ -8,19 +8,19 @@ import { ProductsContext } from "./ProductsContext";
import { AppEnv } from "@autumn/shared";
import CreateProduct from "./CreateProduct";
import { ProductsTable } from "./ProductsTable";
-import { CouponsTable } from "./coupons/CouponsTable";
-import CreateCoupon from "./coupons/CreateCoupon";
import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
import { Ticket } from "lucide-react";
import React from "react";
-import CreateRewardTrigger from "./reward-triggers/CreateRewardTriger";
+
import { RewardTriggersTable } from "./reward-triggers/RewardTriggersTable";
import CreateRewardTriggerModal from "./reward-triggers/CreateRewardTriger";
+import { RewardsTable } from "./rewards/RewardsTAble";
+import CreateReward from "./rewards/CreateReward";
function ProductsView({ env }: { env: AppEnv }) {
const [selectedProduct, setSelectedProduct] = useState(null);
- const [showCoupons, setShowCoupons] = useState(false);
+ const [showRewards, setShowRewards] = useState(false);
const { data, isLoading, mutate } = useAxiosSWR({
url: `/products/data`,
env: env,
@@ -38,8 +38,8 @@ function ProductsView({ env }: { env: AppEnv }) {
setSelectedProduct(data.products[0]);
}
- if (data?.coupons.length > 0) {
- setShowCoupons(true);
+ if (data?.rewards.length > 0) {
+ setShowRewards(true);
}
}, [data]);
@@ -65,9 +65,9 @@ function ProductsView({ env }: { env: AppEnv }) {
0}
- onClick={() => setShowCoupons((prev) => !prev)}
+ show={showRewards}
+ disabled={data?.rewards.length > 0}
+ onClick={() => setShowRewards((prev) => !prev)}
>
Coupons
@@ -75,7 +75,7 @@ function ProductsView({ env }: { env: AppEnv }) {
- {showCoupons && (
+ {showRewards && (
@@ -86,8 +86,8 @@ function ProductsView({ env }: { env: AppEnv }) {
{/* (eg, 10% off all products). */}
-
-
+
+
diff --git a/vite/src/views/products/reward-triggers/CreateRewardTriger.tsx b/vite/src/views/products/reward-triggers/CreateRewardTriger.tsx
index a22ea11df..f9603b1e5 100644
--- a/vite/src/views/products/reward-triggers/CreateRewardTriger.tsx
+++ b/vite/src/views/products/reward-triggers/CreateRewardTriger.tsx
@@ -19,9 +19,9 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
import { toast } from "sonner";
import { PlusIcon } from "lucide-react";
import {
- Coupon,
+ Reward,
CouponDurationType,
- CreateCoupon as CreateCouponType,
+ CreateReward as CreateCouponType,
DiscountType,
RewardTrigger,
RewardTriggerEvent,
diff --git a/vite/src/views/products/reward-triggers/RewardTriggerConfig.tsx b/vite/src/views/products/reward-triggers/RewardTriggerConfig.tsx
index 23d7945f5..334bb3d5a 100644
--- a/vite/src/views/products/reward-triggers/RewardTriggerConfig.tsx
+++ b/vite/src/views/products/reward-triggers/RewardTriggerConfig.tsx
@@ -7,7 +7,7 @@ import {
SelectItem,
SelectValue,
} from "@/components/ui/select";
-import { RewardTrigger, Coupon, RewardTriggerEvent } from "@autumn/shared";
+import { RewardTrigger, Reward, RewardTriggerEvent } from "@autumn/shared";
import { useProductsContext } from "../ProductsContext";
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
import { useState } from "react";
@@ -50,7 +50,7 @@ export const RewardTriggerConfig = ({
/>
-
Coupon
+
Reward