From ab655107fc7a2325a3464061297525b0e4e5290f Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Fri, 19 Sep 2025 23:21:05 +0530 Subject: [PATCH 1/6] changes to have view for the updaterewardprogram and also the service for it --- .../products/RewardProgramService.tsx | 37 +++++++++ .../reward-programs/RewardProgramsTable.tsx | 7 ++ .../reward-programs/UpdateRewardPrograms.tsx | 83 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 vite/src/services/products/RewardProgramService.tsx create mode 100644 vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx diff --git a/vite/src/services/products/RewardProgramService.tsx b/vite/src/services/products/RewardProgramService.tsx new file mode 100644 index 000000000..3e6c8214d --- /dev/null +++ b/vite/src/services/products/RewardProgramService.tsx @@ -0,0 +1,37 @@ +import { RewardProgram, CreateRewardProgram } from "@autumn/shared"; + +import { AxiosInstance } from "axios"; + +export class RewardProgramService { + static async createReward({ + axiosInstance, + data, + }: { + axiosInstance: AxiosInstance; + data: CreateRewardProgram; + }) { + await axiosInstance.post("/v1/reward_programs", data); + } + + static async deleteReward({ + axiosInstance, + internalId, + }: { + axiosInstance: AxiosInstance; + internalId: string; + }) { + await axiosInstance.delete(`/v1/reward_programs/${internalId}`); + } + + static async updateReward({ + axiosInstance, + internalId, + data, + }: { + axiosInstance: AxiosInstance; + internalId: string; + data: RewardProgram; + }) { + await axiosInstance.post(`/v1/reward_programs/${internalId}`, data); + } +} diff --git a/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx b/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx index aa0a0740d..f22b8d145 100644 --- a/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx +++ b/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx @@ -8,6 +8,7 @@ import { Item, Row } from "@/components/general/TableGrid"; import { AdminHover } from "@/components/general/AdminHover"; import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; import { RewardProgramRowToolbar } from "./RewardProgramRowToolbar"; +import UpdateRewardProgram from "./UpdateRewardPrograms"; export const RewardProgramsTable = () => { const { rewardPrograms } = useRewardsQuery(); @@ -17,6 +18,12 @@ export const RewardProgramsTable = () => { return ( <> + {/* */} {rewardPrograms && rewardPrograms.length > 0 ? ( diff --git a/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx b/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx new file mode 100644 index 000000000..00b4f4ee1 --- /dev/null +++ b/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { toast } from "sonner"; +import { Reward } from "@autumn/shared"; +import { useEnv } from "@/utils/envUtils"; +import { RewardService } from "@/services/products/RewardService"; +import { getBackendErr } from "@/utils/genUtils"; +import { WarningBox } from "@/components/general/modal-components/WarningBox"; +import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; +import { RewardProgramConfig } from "./RewardProgramConfig"; +import { RewardProgramService } from "@/services/products/RewardProgramService"; + +function UpdateRewardProgram({ + open, + setOpen, + selectedReward, + setSelectedReward, +}: { + open: boolean; + setOpen: (open: boolean) => void; + selectedReward: Reward | null; + setSelectedReward: (reward: Reward) => void; +}) { + const [updateLoading, setUpdateLoading] = useState(false); + const { refetch } = useRewardsQuery(); + + const env = useEnv(); + const axiosInstance = useAxiosInstance({ env }); + + const handleUpdate = async () => { + setUpdateLoading(true); + try { + await RewardProgramService.updateReward({ + axiosInstance, + internalId: selectedReward!.internal_id, + data: selectedReward!, + }); + toast.success("Reward updated successfully"); + await refetch(); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to update coupon")); + } + setUpdateLoading(false); + }; + + return ( + + + Update Reward Programs + + Existing customers with this coupon will not be affected + + + {selectedReward && ( + + )} + + + + + + + ); +} + +export default UpdateRewardProgram; From 86222adfa909a0e9bbdf19e02f633754a38d37c1 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Sat, 20 Sep 2025 00:20:44 +0530 Subject: [PATCH 2/6] `feat(rewardPrograms): Update RewardProgramService to support idOrInternalId and add update endpoint` --- .../api/rewards/rewardProgramRouter.ts | 76 ++++++++++++++++++- .../internal/rewards/RewardProgramService.ts | 74 ++++++++++++++---- .../products/RewardProgramService.tsx | 13 +++- 3 files changed, 143 insertions(+), 20 deletions(-) diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 70db20ddc..0cfd9dfab 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -40,7 +40,7 @@ rewardProgramRouter.post("", (req, res) => let existingProgram = await RewardProgramService.get({ db, - id: body.id, + idOrInternalId: body.id, orgId, env, }); @@ -86,7 +86,7 @@ rewardProgramRouter.post("", (req, res) => return res.status(200).json(createdRewardProgram); }, - }), + }) ); rewardProgramRouter.delete("/:id", (req, res) => @@ -100,12 +100,80 @@ rewardProgramRouter.delete("/:id", (req, res) => let rewardProgram = await RewardProgramService.delete({ db, - id, + idOrInternalId: id, orgId, env, }); return res.status(200).json(rewardProgram); }, - }), + }) +); + +rewardProgramRouter.put("/:id", (req, res) => + routeHandler({ + req, + res, + action: "update reward program", + handler: async (req: any, res: any) => { + const { orgId, env, db } = req; + const { idOrInternalId } = req.params; + const body = req.body; + + if (!body.internal_reward_id) { + throw new RecaseError({ + message: "Please select a reward to link this program to", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + // Ensure program exists + let existingProgram = await RewardProgramService.get({ + db, + idOrInternalId, + orgId, + env, + }); + + if (!existingProgram) { + throw new RecaseError({ + message: `Program with ID ${idOrInternalId} does not exist`, + code: ErrCode.InvalidRequest, + statusCode: 404, + }); + } + + const rewardProgram = constructRewardProgram({ + rewardProgramData: CreateRewardProgram.parse({ + ...body, + idOrInternalId, // enforce consistency with URL param + }), + orgId, + env, + }); + + if ( + rewardProgram.when == RewardTriggerEvent.Checkout && + (nullish(rewardProgram.product_ids) || + rewardProgram.product_ids!.length == 0) + ) { + throw new RecaseError({ + message: "If redeem on checkout, must specify at least one product", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + let updatedRewardProgram = await RewardProgramService.update({ + db, + idOrInternalId, + orgId, + env, + data: rewardProgram, + }); + + return res.status(200).json(updatedRewardProgram); + }, + }) ); diff --git a/server/src/internal/rewards/RewardProgramService.ts b/server/src/internal/rewards/RewardProgramService.ts index 67ed56410..4b5791435 100644 --- a/server/src/internal/rewards/RewardProgramService.ts +++ b/server/src/internal/rewards/RewardProgramService.ts @@ -1,4 +1,4 @@ -import { and, arrayContains, count, eq, inArray } from "drizzle-orm"; +import { and, arrayContains, count, eq, inArray, or } from "drizzle-orm"; import RecaseError from "@/utils/errorUtils.js"; import { ErrCode, @@ -14,22 +14,25 @@ import { referralCodes, rewardRedemptions } from "@autumn/shared"; export class RewardProgramService { static async get({ db, - id, + idOrInternalId, orgId, env, errorIfNotFound = false, }: { db: DrizzleCli; - id: string; + idOrInternalId: string; orgId: string; env: string; errorIfNotFound?: boolean; }) { let result = await db.query.rewardPrograms.findFirst({ where: and( - eq(rewardPrograms.id, id), + or( + eq(rewardPrograms.id, idOrInternalId), + eq(rewardPrograms.internal_id, idOrInternalId) + ), eq(rewardPrograms.org_id, orgId), - eq(rewardPrograms.env, env), + eq(rewardPrograms.env, env) ), }); @@ -79,7 +82,7 @@ export class RewardProgramService { eq(rewardPrograms.org_id, orgId), eq(rewardPrograms.env, env), eq(rewardPrograms.when, RewardTriggerEvent.Checkout), - arrayContains(rewardPrograms.product_ids, productIds), + arrayContains(rewardPrograms.product_ids, productIds) ), }); @@ -104,7 +107,7 @@ export class RewardProgramService { eq(referralCodes.internal_customer_id, internalCustomerId), eq(referralCodes.internal_reward_program_id, internalRewardProgramId), eq(referralCodes.org_id, orgId), - eq(referralCodes.env, env), + eq(referralCodes.env, env) ), }); @@ -139,12 +142,12 @@ export class RewardProgramService { static async delete({ db, - id, + idOrInternalId, orgId, env, }: { db: DrizzleCli; - id: string; + idOrInternalId: string; orgId: string; env: string; }) { @@ -152,10 +155,13 @@ export class RewardProgramService { .delete(rewardPrograms) .where( and( - eq(rewardPrograms.id, id), + or( + eq(rewardPrograms.id, idOrInternalId), + eq(rewardPrograms.internal_id, idOrInternalId) + ), eq(rewardPrograms.org_id, orgId), - eq(rewardPrograms.env, env), - ), + eq(rewardPrograms.env, env) + ) ) .returning(); @@ -187,7 +193,7 @@ export class RewardProgramService { where: and( eq(referralCodes.code, code), eq(referralCodes.org_id, orgId), - eq(referralCodes.env, env), + eq(referralCodes.env, env) ), with: withRewardProgram ? { @@ -247,10 +253,48 @@ export class RewardProgramService { .where( and( eq(rewardRedemptions.referral_code_id, referralCodeId), - eq(rewardRedemptions.triggered, true), - ), + eq(rewardRedemptions.triggered, true) + ) ); return result[0].count; } + + static async update({ + db, + idOrInternalId, + orgId, + env, + data, + }: { + db: DrizzleCli; + idOrInternalId: string; + orgId: string; + env: string; + data: RewardProgram; + }) { + let result = await db + .update(rewardPrograms) + .set(data as any) + .where( + and( + or( + eq(rewardPrograms.id, idOrInternalId), + eq(rewardPrograms.internal_id, idOrInternalId) + ), + eq(rewardPrograms.org_id, orgId), + eq(rewardPrograms.env, env) + ) + ) + .returning(); + + if (result.length === 0) { + throw new RecaseError({ + message: "Reward program not found", + code: ErrCode.RewardNotFound, + }); + } + + return result[0] as RewardProgram; + } } diff --git a/vite/src/services/products/RewardProgramService.tsx b/vite/src/services/products/RewardProgramService.tsx index 3e6c8214d..baa83690f 100644 --- a/vite/src/services/products/RewardProgramService.tsx +++ b/vite/src/services/products/RewardProgramService.tsx @@ -32,6 +32,17 @@ export class RewardProgramService { internalId: string; data: RewardProgram; }) { - await axiosInstance.post(`/v1/reward_programs/${internalId}`, data); + try { + const res = await axiosInstance.put( + `/v1/reward_programs/${internalId}`, + data + ); + return res.data; + } catch (err: any) { + // maybe rethrow as your RecaseError or wrap + throw new Error( + err.response?.data?.message || "Failed to update reward program" + ); + } } } From 1f609eb59249dd9a28d0a2c529515b143db6f067 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Sat, 20 Sep 2025 00:53:46 +0530 Subject: [PATCH 3/6] changes for the update logic for passing proper id --- .../internal/api/rewards/rewardProgramRouter.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 0cfd9dfab..8243c473f 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -117,7 +117,7 @@ rewardProgramRouter.put("/:id", (req, res) => action: "update reward program", handler: async (req: any, res: any) => { const { orgId, env, db } = req; - const { idOrInternalId } = req.params; + const { id } = req.params; const body = req.body; if (!body.internal_reward_id) { @@ -131,14 +131,16 @@ rewardProgramRouter.put("/:id", (req, res) => // Ensure program exists let existingProgram = await RewardProgramService.get({ db, - idOrInternalId, + idOrInternalId: id, orgId, env, }); + console.log("Existing program:", existingProgram); + if (!existingProgram) { throw new RecaseError({ - message: `Program with ID ${idOrInternalId} does not exist`, + message: `Program with ID ${id} does not exist`, code: ErrCode.InvalidRequest, statusCode: 404, }); @@ -147,12 +149,14 @@ rewardProgramRouter.put("/:id", (req, res) => const rewardProgram = constructRewardProgram({ rewardProgramData: CreateRewardProgram.parse({ ...body, - idOrInternalId, // enforce consistency with URL param + id: existingProgram.id, // ID cannot be changed }), orgId, env, }); + console.log("Updating program to:", rewardProgram); + if ( rewardProgram.when == RewardTriggerEvent.Checkout && (nullish(rewardProgram.product_ids) || @@ -167,7 +171,7 @@ rewardProgramRouter.put("/:id", (req, res) => let updatedRewardProgram = await RewardProgramService.update({ db, - idOrInternalId, + idOrInternalId: id, orgId, env, data: rewardProgram, From b3952a270260ff5d9d825e9e5e0cec554bb67df2 Mon Sep 17 00:00:00 2001 From: Muhammad Usman Date: Sat, 20 Sep 2025 08:28:35 -0700 Subject: [PATCH 4/6] Fix: Create product prevent API call if product name field is empty and not valid --- .../products/components/CreateProductDialog.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vite/src/views/products/products/components/CreateProductDialog.tsx b/vite/src/views/products/products/components/CreateProductDialog.tsx index d52971ed7..ca64819db 100644 --- a/vite/src/views/products/products/components/CreateProductDialog.tsx +++ b/vite/src/views/products/products/components/CreateProductDialog.tsx @@ -43,6 +43,17 @@ function CreateProduct({ const navigate = useNavigate(); const handleCreateClicked = async () => { + const productName = product.name?.trim() || ""; + + if (!/^[a-zA-Z0-9 _-]+$/.test(productName)) { + toast.error( + !productName + ? "Product name is required" + : "Product name can only contain alphanumeric characters, dashes (-), and underscores (_)" + ); + return; + } + setLoading(true); try { const newProduct = await ProductService.createProduct( From 7b1b2cd278231d49f228928830d77b8a47a00383 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 22 Sep 2025 13:12:39 +0100 Subject: [PATCH 5/6] fix: show error message for update reward program --- .../api/rewards/rewardProgramRouter.ts | 7 ++-- .../products/RewardProgramService.tsx | 13 +------- .../reward-programs/RewardProgramConfig.tsx | 3 ++ .../reward-programs/RewardProgramsTable.tsx | 4 +-- .../reward-programs/UpdateRewardPrograms.tsx | 32 +++++++++---------- 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 8243c473f..2edbb5ea9 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -136,7 +136,7 @@ rewardProgramRouter.put("/:id", (req, res) => env, }); - console.log("Existing program:", existingProgram); + // console.log("Existing program:", existingProgram); if (!existingProgram) { throw new RecaseError({ @@ -155,7 +155,7 @@ rewardProgramRouter.put("/:id", (req, res) => env, }); - console.log("Updating program to:", rewardProgram); + // console.log("Updating program to:", rewardProgram); if ( rewardProgram.when == RewardTriggerEvent.Checkout && @@ -163,7 +163,8 @@ rewardProgramRouter.put("/:id", (req, res) => rewardProgram.product_ids!.length == 0) ) { throw new RecaseError({ - message: "If redeem on checkout, must specify at least one product", + message: + "When `Redeem On` is set to `Checkout`, must specify at least one product", code: ErrCode.InvalidRequest, statusCode: 400, }); diff --git a/vite/src/services/products/RewardProgramService.tsx b/vite/src/services/products/RewardProgramService.tsx index baa83690f..c06a56364 100644 --- a/vite/src/services/products/RewardProgramService.tsx +++ b/vite/src/services/products/RewardProgramService.tsx @@ -32,17 +32,6 @@ export class RewardProgramService { internalId: string; data: RewardProgram; }) { - try { - const res = await axiosInstance.put( - `/v1/reward_programs/${internalId}`, - data - ); - return res.data; - } catch (err: any) { - // maybe rethrow as your RecaseError or wrap - throw new Error( - err.response?.data?.message || "Failed to update reward program" - ); - } + await axiosInstance.put(`/v1/reward_programs/${internalId}`, data); } } diff --git a/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx b/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx index 340b5954f..25704edcf 100644 --- a/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx +++ b/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx @@ -38,9 +38,11 @@ import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; export const RewardProgramConfig = ({ rewardProgram, setRewardProgram, + isUpdate, }: { rewardProgram: RewardProgram; setRewardProgram: (rewardProgram: RewardProgram) => void; + isUpdate?: boolean; }) => { const { rewards } = useRewardsQuery(); @@ -63,6 +65,7 @@ export const RewardProgramConfig = ({ onValueChange={(value) => setRewardProgram({ ...rewardProgram, internal_reward_id: value }) } + // disabled={isUpdate} > diff --git a/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx b/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx index f22b8d145..1bab00956 100644 --- a/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx +++ b/vite/src/views/products/rewards/reward-programs/RewardProgramsTable.tsx @@ -21,8 +21,8 @@ export const RewardProgramsTable = () => { {/* */} diff --git a/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx b/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx index 00b4f4ee1..9108fd48a 100644 --- a/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx +++ b/vite/src/views/products/rewards/reward-programs/UpdateRewardPrograms.tsx @@ -8,9 +8,8 @@ import { import { Button } from "@/components/ui/button"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { toast } from "sonner"; -import { Reward } from "@autumn/shared"; +import { RewardProgram } from "@autumn/shared"; import { useEnv } from "@/utils/envUtils"; -import { RewardService } from "@/services/products/RewardService"; import { getBackendErr } from "@/utils/genUtils"; import { WarningBox } from "@/components/general/modal-components/WarningBox"; import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; @@ -20,13 +19,13 @@ import { RewardProgramService } from "@/services/products/RewardProgramService"; function UpdateRewardProgram({ open, setOpen, - selectedReward, - setSelectedReward, + selectedRewardProgram, + setSelectedRewardProgram, }: { open: boolean; setOpen: (open: boolean) => void; - selectedReward: Reward | null; - setSelectedReward: (reward: Reward) => void; + selectedRewardProgram: RewardProgram | null; + setSelectedRewardProgram: (reward: RewardProgram) => void; }) { const [updateLoading, setUpdateLoading] = useState(false); const { refetch } = useRewardsQuery(); @@ -39,14 +38,14 @@ function UpdateRewardProgram({ try { await RewardProgramService.updateReward({ axiosInstance, - internalId: selectedReward!.internal_id, - data: selectedReward!, + internalId: selectedRewardProgram!.internal_id, + data: selectedRewardProgram!, }); toast.success("Reward updated successfully"); await refetch(); setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Failed to update coupon")); + toast.error(getBackendErr(error, "Failed to update reward program")); } setUpdateLoading(false); }; @@ -54,15 +53,16 @@ function UpdateRewardProgram({ return ( - Update Reward Programs - - Existing customers with this coupon will not be affected - + Update Reward Program + {/* + Existing customers with this reward program will not be affected + */} - {selectedReward && ( + {selectedRewardProgram && ( )} From 59aaf3c43067e9eafa97bcf194424337a0c320a0 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 22 Sep 2025 13:17:56 +0100 Subject: [PATCH 6/6] Fixes/Update-logic-reward-program --- .../handleCusDiscountDeleted.ts | 267 +++++++++--------- .../api/rewards/rewardProgramRouter.ts | 4 +- 2 files changed, 134 insertions(+), 137 deletions(-) diff --git a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts index 9b292e000..192c61d2c 100644 --- a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts @@ -7,164 +7,163 @@ import { notNullish } from "@/utils/genUtils.js"; import { createStripeCli } from "../utils.js"; export async function handleCusDiscountDeleted({ - db, - org, - discount, - env, - logger, - res, + db, + org, + discount, + env, + logger, + res, }: { - db: DrizzleCli; - org: any; - discount: any; - env: any; - logger: any; - res: any; + db: DrizzleCli; + org: any; + discount: any; + env: any; + logger: any; + res: any; }) { - const customer = await CusService.getByStripeId({ - db, - stripeId: discount.customer, - }); + const customer = await CusService.getByStripeId({ + db, + stripeId: discount.customer, + }); - if (!customer) { - logger.warn(`discount.deleted: customer ${discount.customer} not found`); - return; - } + if (!customer) { + logger.warn(`discount.deleted: customer ${discount.customer} not found`); + return; + } - if (customer.env !== env || customer.org_id !== org.id) { - logger.info( - `discount.deleted: env or org mismatch, skipping, ${customer.env} !== ${env} || ${customer.org_id} !== ${org.id}`, - ); - return; - } + if (customer.env !== env || customer.org_id !== org.id) { + logger.info( + `discount.deleted: env or org mismatch, skipping, ${customer.env} !== ${env} || ${customer.org_id} !== ${org.id}` + ); + return; + } - // Check if any redemptions available, and apply to customer if so - const redemptions = await RewardRedemptionService.getUnappliedRedemptions({ - db, - internalCustomerId: customer.internal_id, - }); + // Check if any redemptions available, and apply to customer if so + const redemptions = await RewardRedemptionService.getUnappliedRedemptions({ + db, + internalCustomerId: customer.internal_id, + }); - logger.info( - `discount.deleted:, discount ID: ${discount.id}, found ${redemptions.length} redemptions`, - ); + logger.info( + `discount.deleted:, discount ID: ${discount.id}, found ${redemptions.length} redemptions` + ); - if (redemptions.length == 0) return; + if (redemptions.length == 0) return; - const paidProductRedemption = redemptions.find( - (r) => - r.reward_program.reward.id === - (typeof discount.coupon == "string" - ? discount.coupon - : discount.coupon.id), - ); + const paidProductRedemption = redemptions.find( + (r) => + r.reward_program.reward.id === + (typeof discount.coupon == "string" + ? discount.coupon + : discount.coupon.id) + ); - if (discount.subscription) { - logger.info( - `Discount is a subscription, paidProductRedemption: ${paidProductRedemption?.id}`, - ); + if (discount.subscription) { + logger.info( + `Discount is a subscription, paidProductRedemption: ${paidProductRedemption?.id}` + ); - if (!paidProductRedemption) return; + if (!paidProductRedemption) return; - // Re-apply coupon and mark applied / redeemer applied to true - const stripeCli = createStripeCli({ - org, - env, - }); + // Re-apply coupon and mark applied / redeemer applied to true + const stripeCli = createStripeCli({ + org, + env, + }); - // Mark reward redemption as applied / redeemer applied to true + // Mark reward redemption as applied / redeemer applied to true + const sub = await stripeCli.subscriptions.retrieve(discount.subscription); - const sub = await stripeCli.subscriptions.retrieve(discount.subscription); + // can't really test because it modifies subscription affected by test clock... + try { + await stripeCli.subscriptions.update(discount.subscription, { + discounts: [ + ...(sub.discounts as string[]).map((d: string) => ({ + discount: d, + })), + { + coupon: paidProductRedemption.reward_program.reward.id as string, + }, + ], + }); + } catch (error: any) { + logger.error( + `Failed to update subscription ${discount.subscription} with paid product coupon, error: ${error.message}` + ); + throw error; + } - // can't really test because it modifies subscription affected by test clock... - try { - await stripeCli.subscriptions.update(discount.subscription, { - discounts: [ - ...(sub.discounts as string[]).map((d: string) => ({ - discount: d, - })), - { - coupon: paidProductRedemption.reward_program.reward.id as string, - }, - ], - }); - } catch (error: any) { - logger.error( - `Failed to update subscription ${discount.subscription} with paid product coupon, error: ${error.message}`, - ); - throw error; - } + // Mark reward redemption as applied / redeemer applied to true + const isReferrer = + paidProductRedemption.referral_code.internal_customer_id === + customer.internal_id; - // Mark reward redemption as applied / redeemer applied to true - const isReferrer = - paidProductRedemption.referral_code.internal_customer_id === - customer.internal_id; + await RewardRedemptionService.update({ + db, + id: paidProductRedemption.id, + updates: { + applied: isReferrer ? true : undefined, + redeemer_applied: isReferrer ? undefined : true, + }, + }); - await RewardRedemptionService.update({ - db, - id: paidProductRedemption.id, - updates: { - applied: isReferrer ? true : undefined, - redeemer_applied: isReferrer ? undefined : true, - }, - }); + return; + } - return; - } + const redemption = redemptions[0]; - const redemption = redemptions[0]; + // Apply redemption to customer + const stripeCli = createStripeCli({ + org, + env, + }); - // Apply redemption to customer - const stripeCli = createStripeCli({ - org, - env, - }); + const stripeCus = (await stripeCli.customers.retrieve( + discount.customer + )) as Stripe.Customer; - const 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; + } - if (stripeCus && notNullish(stripeCus.discount)) { - logger.info( - `discount.deleted: stripe customer ${discount.customer} already has a discount`, - ); - return; - } + const reward = await RewardService.get({ + db, + orgId: org.id, + env, + idOrInternalId: redemption.reward_program.internal_reward_id!, + }); - const reward = await RewardService.get({ - db, - orgId: org.id, - env, - idOrInternalId: redemption.reward_program.internal_reward_id!, - }); + if (!reward) { + logger.warn( + `discount.deleted: reward ${redemption.reward_program.internal_id} not found` + ); + return; + } - if (!reward) { - logger.warn( - `discount.deleted: reward ${redemption.reward_program.internal_id} not found`, - ); - return; - } + const legacyStripe = createStripeCli({ + org, + env, + legacyVersion: true, + }); - const legacyStripe = createStripeCli({ - org, - env, - legacyVersion: true, - }); + await legacyStripe.customers.update(discount.customer, { + // @ts-expect-error + coupon: reward.id, + }); - await legacyStripe.customers.update(discount.customer, { - // @ts-expect-error - coupon: reward.id, - }); + await RewardRedemptionService.update({ + db, + id: redemption.id, + updates: { + applied: true, + }, + }); - await RewardRedemptionService.update({ - db, - id: redemption.id, - updates: { - applied: true, - }, - }); - - logger.info( - `discount.deleted: applied reward ${reward.name} on customer ${customer.name} (${customer.id})`, - ); - logger.info(`Redemption ID: ${redemption.id}`); + logger.info( + `discount.deleted: applied reward ${reward.name} on customer ${customer.name} (${customer.id})` + ); + logger.info(`Redemption ID: ${redemption.id}`); } diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 17c6c33d0..8f183eeca 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -46,8 +46,6 @@ rewardProgramRouter.put("/:id", (req, res) => env, }); - // console.log("Existing program:", existingProgram); - if (!existingProgram) { throw new RecaseError({ message: `Program with ID ${id} does not exist`, @@ -65,7 +63,7 @@ rewardProgramRouter.put("/:id", (req, res) => env, }); - // console.log("Updating program to:", rewardProgram); + // Update on existing redemptions? (should be none unless affecting stacked rewards...) if ( rewardProgram.when == RewardTriggerEvent.Checkout &&