moving to qa referrals
This commit is contained in:
11
server/src/external/stripe/stripeWebhooks.ts
vendored
11
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -18,6 +18,7 @@ import {
|
||||
createLogtail,
|
||||
createLogtailWithContext,
|
||||
} from "../logtail/logtailUtils.js";
|
||||
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
|
||||
|
||||
export const stripeWebhookRouter = express.Router();
|
||||
|
||||
@@ -158,6 +159,16 @@ stripeWebhookRouter.post(
|
||||
logger,
|
||||
});
|
||||
break;
|
||||
|
||||
case "customer.discount.deleted":
|
||||
await handleCusDiscountDeleted({
|
||||
sb: request.sb,
|
||||
org,
|
||||
discount: event.data.object,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
handleRequestError({
|
||||
|
||||
71
server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
vendored
Normal file
71
server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
||||
import { createStripeCli } from "../utils.js";
|
||||
|
||||
export async function handleCusDiscountDeleted({
|
||||
sb,
|
||||
org,
|
||||
discount,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
sb: any;
|
||||
org: any;
|
||||
discount: any;
|
||||
env: any;
|
||||
logger: any;
|
||||
}) {
|
||||
let customer = await CusService.getByStripeId({
|
||||
sb,
|
||||
stripeId: discount.customer,
|
||||
});
|
||||
|
||||
if (customer.env !== env || customer.org_id !== org.id) {
|
||||
logger.info(`discount.deleted: env or org mismatch, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customer) {
|
||||
logger.warn(`discount.deleted: customer ${discount.customer} 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(
|
||||
// `discount.deleted: no redemptions available for customer ${customer.id}`
|
||||
// );
|
||||
return;
|
||||
}
|
||||
|
||||
let redemption = redemptions[0];
|
||||
let reward = redemption.reward_trigger.reward;
|
||||
|
||||
// Apply redemption to customer
|
||||
let stripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
await stripeCli.customers.update(discount.customer, {
|
||||
coupon: reward.internal_id,
|
||||
});
|
||||
|
||||
await RewardRedemptionService.update({
|
||||
sb,
|
||||
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}`);
|
||||
}
|
||||
@@ -15,10 +15,8 @@ import { invoiceRouter } from "./customers/invoiceRouter.js";
|
||||
import { entityRouter } from "./entities/entityRouter.js";
|
||||
import { migrationRouter } from "./migrations/migrationRouter.js";
|
||||
import rewardRouter from "./rewards/rewardRouter.js";
|
||||
import {
|
||||
referralRouter,
|
||||
rewardTriggerRouter,
|
||||
} from "./rewards/rewardTriggerRouter.js";
|
||||
import { rewardTriggerRouter } from "./rewards/rewardTriggerRouter.js";
|
||||
import { referralRouter } from "./rewards/referralRouter.js";
|
||||
|
||||
const apiRouter = Router();
|
||||
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
import {
|
||||
AppEnv,
|
||||
CreateCustomer,
|
||||
CreateCustomerSchema,
|
||||
CusProductStatus,
|
||||
Customer,
|
||||
CustomerResponseSchema,
|
||||
} from "@autumn/shared";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
|
||||
import { CreateCustomerSchema, CustomerResponseSchema } from "@autumn/shared";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { ErrorMessages } from "@/errors/errMessages.js";
|
||||
import { Router } from "express";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { CusService } from "../../customers/CusService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { EventService } from "../events/EventService.js";
|
||||
import { getCustomerDetails } from "./cusUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { getCusBalancesByEntitlement } from "@/internal/customers/entitlements/cusEntUtils.js";
|
||||
import {
|
||||
fullCusProductToCusEnts,
|
||||
fullCusProductToCusPrices,
|
||||
} from "@/internal/customers/products/cusProductUtils.js";
|
||||
import { deleteCusById } from "./handlers/cusDeleteHandlers.js";
|
||||
import { handleUpdateBalances } from "./handlers/handleUpdateBalances.js";
|
||||
import { handleUpdateEntitlement } from "./handlers/handleUpdateEntitlement.js";
|
||||
import { handleCusProductExpired } from "./handlers/handleCusProductExpired.js";
|
||||
import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js";
|
||||
|
||||
import { handlePostCustomerRequest } from "./handlers/handleCreateCustomer.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { entityRouter } from "../entities/entityRouter.js";
|
||||
@@ -69,7 +56,6 @@ cusRouter.get("", async (req: any, res: any) => {
|
||||
|
||||
cusRouter.post("", handlePostCustomerRequest);
|
||||
|
||||
|
||||
// BY CUSTOMER ID
|
||||
|
||||
cusRouter.get("/:customer_id", async (req: any, res: any) => {
|
||||
@@ -94,7 +80,8 @@ cusRouter.get("/:customer_id", async (req: any, res: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { main, addOns, balances, invoices } = await getCustomerDetails({
|
||||
// const { main, addOns, balances, invoices } =
|
||||
let cusData = await getCustomerDetails({
|
||||
customer,
|
||||
sb: req.sb,
|
||||
orgId: req.orgId,
|
||||
@@ -103,13 +90,7 @@ cusRouter.get("/: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" });
|
||||
}
|
||||
@@ -131,8 +112,6 @@ cusRouter.delete("/:customer_id", async (req: any, res: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
cusRouter.post("/:customer_id", async (req: any, res: any) => {
|
||||
try {
|
||||
const customerId = req.params.customer_id;
|
||||
@@ -311,8 +290,6 @@ cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus);
|
||||
|
||||
cusRouter.use("/:customer_id/entities", entityRouter);
|
||||
|
||||
|
||||
|
||||
// cusRouter.put("", async (req: any, res: any) => {
|
||||
// try {
|
||||
// const { id, name, email, fingerprint, reset_at } = req.body;
|
||||
@@ -373,4 +350,4 @@ cusRouter.use("/:customer_id/entities", entityRouter);
|
||||
// } catch (error) {
|
||||
// handleRequestError({ req, error, res, action: "update customer" });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CusProductStatus,
|
||||
Customer,
|
||||
CustomerData,
|
||||
CustomerResponseSchema,
|
||||
CustomerSchema,
|
||||
ErrCode,
|
||||
FullCusProduct,
|
||||
@@ -62,7 +63,7 @@ export const updateCustomerDetails = async ({
|
||||
if (!customer.email && customerData?.email) {
|
||||
updates.email = customerData.email;
|
||||
}
|
||||
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
logger.info(`Updating customer details`, { updates });
|
||||
customer = await CusService.update({
|
||||
@@ -73,8 +74,7 @@ export const updateCustomerDetails = async ({
|
||||
}
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const getOrCreateCustomer = async ({
|
||||
sb,
|
||||
@@ -94,7 +94,7 @@ export const getOrCreateCustomer = async ({
|
||||
skipGet?: boolean;
|
||||
}) => {
|
||||
let customer;
|
||||
|
||||
|
||||
if (!skipGet) {
|
||||
customer = await CusService.getByIdOrInternalId({
|
||||
sb,
|
||||
@@ -119,7 +119,7 @@ export const getOrCreateCustomer = async ({
|
||||
},
|
||||
logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customer = await updateCustomerDetails({
|
||||
sb,
|
||||
@@ -342,11 +342,18 @@ export const getCustomerDetails = async ({
|
||||
org,
|
||||
});
|
||||
|
||||
// return {
|
||||
// customer,
|
||||
// main,
|
||||
// addOns,
|
||||
// balances,
|
||||
// invoices: processedInvoices,
|
||||
// };
|
||||
return {
|
||||
customer,
|
||||
main,
|
||||
addOns,
|
||||
balances,
|
||||
customer: CustomerResponseSchema.parse(customer),
|
||||
products: main,
|
||||
add_ons: addOns,
|
||||
entitlements: balances,
|
||||
invoices: processedInvoices,
|
||||
};
|
||||
};
|
||||
@@ -399,7 +406,6 @@ export const getCusEntsInFeatures = async ({
|
||||
}
|
||||
|
||||
sortCusEntsForDeduction(cusEnts, reverseOrder);
|
||||
|
||||
|
||||
if (!withPrices) {
|
||||
return { cusEnts, cusPrices: undefined };
|
||||
|
||||
193
server/src/internal/api/rewards/referralRouter.ts
Normal file
193
server/src/internal/api/rewards/referralRouter.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import {
|
||||
generateReferralCode,
|
||||
triggerRedemption,
|
||||
} from "@/internal/rewards/referralUtils.js";
|
||||
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
||||
import { RewardTriggerService } from "@/internal/rewards/RewardTriggerService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { ErrCode, RewardTriggerEvent } from "@autumn/shared";
|
||||
import express from "express";
|
||||
import { ReferralCode, RewardRedemption } from "@autumn/shared";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
export const referralRouter = express.Router();
|
||||
|
||||
// 1. Get referral code
|
||||
referralRouter.post("/code", (req, res) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "get referral code",
|
||||
handler: async (req: any, res: any) => {
|
||||
const { orgId, env, logtail: logger } = req;
|
||||
const { referral_id: rewardTriggerId, customer_id: customerId } =
|
||||
req.body;
|
||||
|
||||
let rewardTrigger = await RewardTriggerService.getById({
|
||||
sb: req.sb,
|
||||
id: rewardTriggerId,
|
||||
orgId,
|
||||
env,
|
||||
errorIfNotFound: true,
|
||||
});
|
||||
|
||||
let customer = await CusService.getById({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
id: customerId,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: "Customer not found",
|
||||
statusCode: 404,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
// Get referral code by customer and reward trigger
|
||||
let existingReferralCode =
|
||||
await RewardTriggerService.getCodeByCustomerAndRewardTrigger({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
internalCustomerId: customer.internal_id,
|
||||
internalRewardTriggerId: rewardTrigger.internal_id,
|
||||
});
|
||||
|
||||
if (existingReferralCode) {
|
||||
return res.status(200).json(existingReferralCode);
|
||||
}
|
||||
|
||||
// Get random 8 letter code
|
||||
const code = generateReferralCode();
|
||||
|
||||
let referralCode: ReferralCode = {
|
||||
code,
|
||||
org_id: orgId,
|
||||
env,
|
||||
internal_customer_id: customer.internal_id,
|
||||
internal_reward_trigger_id: rewardTrigger.internal_id,
|
||||
id: generateId("rc"),
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
await RewardTriggerService.createReferralCode({
|
||||
sb: req.sb,
|
||||
data: referralCode,
|
||||
});
|
||||
|
||||
res.status(200).json(referralCode);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
referralRouter.post("/redeem", (req, res) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "redeem referral code",
|
||||
handler: async (req: any, res: any) => {
|
||||
const { orgId, env, logtail: logger } = req;
|
||||
// const { referral_id: rewardTriggerId } = req.params;
|
||||
const { code, customer_id: customerId } = req.body;
|
||||
|
||||
// 1. Get redeemed by customer, and referral code
|
||||
let [customer, referralCode, org] = await Promise.all([
|
||||
CusService.getById({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
id: customerId,
|
||||
logger,
|
||||
}),
|
||||
RewardTriggerService.getReferralCode({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
code,
|
||||
withRewardTrigger: true,
|
||||
}),
|
||||
OrgService.getFromReq(req),
|
||||
]);
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: "Customer not found",
|
||||
statusCode: 404,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check that code has not reached max redemptions
|
||||
let redemptionCount = await RewardTriggerService.getCodeRedemptionCount({
|
||||
sb: req.sb,
|
||||
referralCodeId: referralCode.id,
|
||||
});
|
||||
|
||||
if (redemptionCount >= referralCode.reward_trigger.max_redemptions) {
|
||||
throw new RecaseError({
|
||||
message: "Referral code has reached max redemptions",
|
||||
statusCode: 400,
|
||||
code: ErrCode.ReferralCodeMaxRedemptionsReached,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Check that customer has not already redeemed a code in this referral program
|
||||
let existingRedemptions = await RewardRedemptionService.getByCustomer({
|
||||
sb: req.sb,
|
||||
internalCustomerId: customer.internal_id,
|
||||
internalRewardTriggerId: referralCode.internal_reward_trigger_id,
|
||||
});
|
||||
|
||||
if (existingRedemptions.length > 0) {
|
||||
throw new RecaseError({
|
||||
message: `Customer ${customer.id} has already redeemed a code in this referral program`,
|
||||
statusCode: 400,
|
||||
code: ErrCode.CustomerAlreadyRedeemedReferralCode,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Insert redemption into db
|
||||
let redemption: RewardRedemption = {
|
||||
id: generateId("rr"),
|
||||
referral_code_id: referralCode.id,
|
||||
internal_customer_id: customer.internal_id, // redeemed by customer
|
||||
internal_reward_trigger_id: referralCode.internal_reward_trigger_id,
|
||||
created_at: Date.now(),
|
||||
triggered:
|
||||
referralCode.reward_trigger.when === RewardTriggerEvent.Immediately,
|
||||
applied: false,
|
||||
updated_at: Date.now(),
|
||||
};
|
||||
|
||||
redemption = await RewardRedemptionService.insert({
|
||||
sb: req.sb,
|
||||
rewardRedemption: redemption,
|
||||
});
|
||||
|
||||
// 5. If reward trigger when is immediate:
|
||||
let { reward_trigger } = referralCode;
|
||||
|
||||
if (referralCode.reward_trigger.when === RewardTriggerEvent.Immediately) {
|
||||
redemption = await triggerRedemption({
|
||||
sb: req.sb,
|
||||
referralCode,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
reward: reward_trigger.reward,
|
||||
redemption,
|
||||
});
|
||||
}
|
||||
|
||||
// Add coupon to customer?
|
||||
res.status(200).json(redemption);
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -1,12 +1,7 @@
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { generateReferralCode } from "@/internal/rewards/referralUtils.js";
|
||||
import { RewardTriggerService } from "@/internal/rewards/RewardTriggerService.js";
|
||||
import { constructRewardTrigger } from "@/internal/rewards/rewardTriggerUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { CreateRewardTrigger, ErrCode } from "@autumn/shared";
|
||||
import { ReferralCode } from "@shared/models/rewardModels/referralModels/referralModels.js";
|
||||
import { CreateRewardTrigger } from "@autumn/shared";
|
||||
import express from "express";
|
||||
|
||||
export const rewardTriggerRouter = express.Router();
|
||||
@@ -24,8 +19,6 @@ rewardTriggerRouter.post("", (req, res) =>
|
||||
env,
|
||||
});
|
||||
|
||||
console.log("rewardTrigger", rewardTrigger);
|
||||
|
||||
let createdRewardTrigger = await RewardTriggerService.createRewardTrigger(
|
||||
{
|
||||
sb: req.sb,
|
||||
@@ -38,130 +31,23 @@ rewardTriggerRouter.post("", (req, res) =>
|
||||
})
|
||||
);
|
||||
|
||||
export const referralRouter = express.Router();
|
||||
|
||||
// 1. Get referral code
|
||||
referralRouter.post("/code", (req, res) =>
|
||||
rewardTriggerRouter.delete("/:id", (req, res) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "get referral code",
|
||||
action: "delete reward trigger",
|
||||
handler: async (req: any, res: any) => {
|
||||
const { orgId, env, logtail: logger } = req;
|
||||
const { referral_id: rewardTriggerId, customer_id: customerId } =
|
||||
req.body;
|
||||
const { orgId, env } = req;
|
||||
const { id } = req.params;
|
||||
|
||||
let rewardTrigger = await RewardTriggerService.getById({
|
||||
let rewardTrigger = await RewardTriggerService.deleteById({
|
||||
sb: req.sb,
|
||||
id: rewardTriggerId,
|
||||
id,
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
|
||||
let customer = await CusService.getById({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
id: customerId,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: "Customer not found",
|
||||
statusCode: 404,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
// Get random 8 letter code
|
||||
const code = generateReferralCode();
|
||||
|
||||
let referralCode: ReferralCode = {
|
||||
code,
|
||||
org_id: orgId,
|
||||
env,
|
||||
internal_customer_id: customer.internal_id,
|
||||
internal_reward_trigger_id: rewardTrigger.internal_id,
|
||||
id: generateId("rc"),
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
await RewardTriggerService.createReferralCode({
|
||||
sb: req.sb,
|
||||
data: referralCode,
|
||||
});
|
||||
|
||||
res.status(200).json(referralCode);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
referralRouter.post("/redeem", (req, res) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "redeem referral code",
|
||||
handler: async (req: any, res: any) => {
|
||||
const { orgId, env, logtail: logger } = req;
|
||||
// const { referral_id: rewardTriggerId } = req.params;
|
||||
const { code, customer_id: customerId } = req.body;
|
||||
|
||||
// Redeemed by customer_id
|
||||
|
||||
let [customer, referralCode] = await Promise.all([
|
||||
CusService.getById({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
id: customerId,
|
||||
logger,
|
||||
}),
|
||||
RewardTriggerService.getReferralCode({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
code,
|
||||
withRewardTrigger: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: "Customer not found",
|
||||
statusCode: 404,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Referral code:", referralCode);
|
||||
|
||||
// let maxRedemptions = referralCode.reward_trigger.max_redemptions;
|
||||
|
||||
let redemptionCount = await RewardTriggerService.getCodeRedemptionCount({
|
||||
sb: req.sb,
|
||||
orgId,
|
||||
env,
|
||||
code,
|
||||
});
|
||||
|
||||
if (redemptionCount >= referralCode.reward_trigger.max_redemptions) {
|
||||
throw new RecaseError({
|
||||
message: "Referral code has reached max redemptions",
|
||||
statusCode: 400,
|
||||
code: ErrCode.ReferralCodeMaxRedemptionsReached,
|
||||
});
|
||||
}
|
||||
|
||||
// console.log("Redemption count:", redemptionCount);
|
||||
|
||||
// Add coupon to customer?
|
||||
|
||||
res.status(200).json({
|
||||
referralCode,
|
||||
});
|
||||
|
||||
// 1. Check how many times code has been redeemed
|
||||
return res.status(200).json(rewardTrigger);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
CusProductStatus,
|
||||
Price,
|
||||
ProcessorType,
|
||||
EntInterval,
|
||||
CustomerEntitlement,
|
||||
CusProduct,
|
||||
FeatureOptions,
|
||||
FreeTrial,
|
||||
BillingType,
|
||||
CollectionMethod,
|
||||
Organization,
|
||||
AppEnv,
|
||||
FullCusProduct,
|
||||
FullCustomerEntitlement,
|
||||
LoggerAction,
|
||||
} from "@autumn/shared";
|
||||
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { getNextEntitlementReset } from "@/utils/timeUtils.js";
|
||||
import { Customer, FeatureType } from "@autumn/shared";
|
||||
import { EntitlementWithFeature, FullProduct } from "@autumn/shared";
|
||||
import { generateId, notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
import { Customer } from "@autumn/shared";
|
||||
import { FullProduct } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { getBillingType, getEntOptions } from "@/internal/prices/priceUtils.js";
|
||||
import { getEntOptions } from "@/internal/prices/priceUtils.js";
|
||||
import { CustomerPrice } from "@autumn/shared";
|
||||
import { CusProductService } from "../products/CusProductService.js";
|
||||
import { InsertCusProductParams } from "../products/AttachParams.js";
|
||||
@@ -37,6 +31,8 @@ import { searchCusProducts } from "@/internal/customers/products/cusProductUtils
|
||||
import { updateOneTimeCusProduct } from "./createOneTimeCusProduct.js";
|
||||
import { initCusEntitlement } from "./initCusEnt.js";
|
||||
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
export const initCusPrice = ({
|
||||
price,
|
||||
customer,
|
||||
@@ -419,6 +415,16 @@ export const createFullCusProduct = async ({
|
||||
cusPrices,
|
||||
});
|
||||
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.TriggerCheckoutReward,
|
||||
payload: {
|
||||
customer,
|
||||
product,
|
||||
org,
|
||||
env: customer.env,
|
||||
},
|
||||
});
|
||||
|
||||
// // Send webhook
|
||||
// await sendSvixEvent({
|
||||
// org: customer.org,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getLatestProducts } from "./productUtils.js";
|
||||
import { CusProductService } from "../customers/products/CusProductService.js";
|
||||
import { CusProdReadService } from "../customers/products/CusProdReadService.js";
|
||||
import { MigrationService } from "../migrations/MigrationService.js";
|
||||
import { RewardTriggerService } from "../rewards/RewardTriggerService.js";
|
||||
|
||||
export const productRouter = Router({ mergeParams: true });
|
||||
|
||||
@@ -26,17 +27,19 @@ productRouter.get("/data", async (req: any, res) => {
|
||||
orgId: req.orgId,
|
||||
});
|
||||
|
||||
const [products, features, org, coupons] = await Promise.all([
|
||||
ProductService.getFullProducts({
|
||||
sb,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
returnAll: true,
|
||||
}),
|
||||
FeatureService.getFromReq(req),
|
||||
OrgService.getFromReq(req),
|
||||
RewardService.getAll({ sb, orgId: req.orgId, env: req.env }),
|
||||
]);
|
||||
const [products, features, org, coupons, rewardTriggers] =
|
||||
await Promise.all([
|
||||
ProductService.getFullProducts({
|
||||
sb,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
returnAll: true,
|
||||
}),
|
||||
FeatureService.getFromReq(req),
|
||||
OrgService.getFromReq(req),
|
||||
RewardService.getAll({ sb, orgId: req.orgId, env: req.env }),
|
||||
RewardTriggerService.getAll({ sb, orgId: req.orgId, env: req.env }),
|
||||
]);
|
||||
|
||||
res.status(200).json({
|
||||
products: getLatestProducts(products),
|
||||
@@ -50,6 +53,7 @@ productRouter.get("/data", async (req: any, res) => {
|
||||
default_currency: org.default_currency,
|
||||
},
|
||||
coupons,
|
||||
rewardTriggers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to get products", error);
|
||||
|
||||
153
server/src/internal/rewards/RewardRedemptionService.ts
Normal file
153
server/src/internal/rewards/RewardRedemptionService.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { RewardRedemption, RewardTriggerEvent } from "@autumn/shared";
|
||||
|
||||
export class RewardRedemptionService {
|
||||
static async getByCustomer({
|
||||
sb,
|
||||
internalCustomerId,
|
||||
internalRewardTriggerId,
|
||||
triggered,
|
||||
withReferralCode = false,
|
||||
withRewardTrigger,
|
||||
triggerWhen,
|
||||
}: {
|
||||
sb: any;
|
||||
internalCustomerId: string;
|
||||
triggered?: boolean;
|
||||
withReferralCode?: boolean;
|
||||
withRewardTrigger?: boolean;
|
||||
internalRewardTriggerId?: string;
|
||||
triggerWhen?: RewardTriggerEvent;
|
||||
}) {
|
||||
let query = sb
|
||||
.from("reward_redemptions")
|
||||
.select(
|
||||
`
|
||||
*
|
||||
${
|
||||
withRewardTrigger
|
||||
? ", reward_trigger:reward_triggers!inner(*, reward:rewards!inner(*))"
|
||||
: ""
|
||||
}
|
||||
${withReferralCode ? ", referral_code:referral_codes!inner(*)" : ""}
|
||||
`
|
||||
)
|
||||
.eq("internal_customer_id", internalCustomerId);
|
||||
|
||||
if (notNullish(internalRewardTriggerId)) {
|
||||
query = query.eq("internal_reward_trigger_id", internalRewardTriggerId);
|
||||
}
|
||||
|
||||
if (notNullish(triggered)) {
|
||||
query = query.eq("triggered", triggered);
|
||||
}
|
||||
|
||||
// if (notNullish(triggerWhen)) {
|
||||
// query = query.eq("reward_trigger.when", triggerWhen);
|
||||
// }
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getByCodeAndCustomer({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
code,
|
||||
internalCustomerId,
|
||||
}: {
|
||||
sb: any;
|
||||
orgId: string;
|
||||
env: string;
|
||||
code: string;
|
||||
internalCustomerId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_redemptions")
|
||||
.select("*")
|
||||
.eq("code", code)
|
||||
.eq("internal_customer_id", internalCustomerId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
static async insert({
|
||||
sb,
|
||||
rewardRedemption,
|
||||
}: {
|
||||
sb: any;
|
||||
rewardRedemption: RewardRedemption;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_redemptions")
|
||||
.insert(rewardRedemption)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async update({
|
||||
sb,
|
||||
id,
|
||||
updates,
|
||||
}: {
|
||||
sb: any;
|
||||
id: string;
|
||||
updates: any;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_redemptions")
|
||||
.update(updates)
|
||||
.eq("id", id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getUnappliedRedemptions({
|
||||
sb,
|
||||
internalCustomerId,
|
||||
}: {
|
||||
sb: any;
|
||||
internalCustomerId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_redemptions")
|
||||
.select(
|
||||
"*, referral_code:referral_codes!inner(*), reward_trigger:reward_triggers!inner(*, reward:rewards!inner(*))"
|
||||
)
|
||||
.eq("referral_code.internal_customer_id", internalCustomerId)
|
||||
.eq("triggered", true)
|
||||
.eq("applied", false);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,73 @@
|
||||
import { RewardTrigger } from "@autumn/shared";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode, RewardTrigger } from "@autumn/shared";
|
||||
import { ReferralCode } from "@shared/models/rewardModels/referralModels/referralModels.js";
|
||||
|
||||
export class RewardTriggerService {
|
||||
static async get({ sb, internalId }: { sb: any; internalId: string }) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_triggers")
|
||||
.select("*, reward:rewards!inner(*)")
|
||||
.eq("internal_id", internalId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
static async getById({
|
||||
sb,
|
||||
id,
|
||||
orgId,
|
||||
env,
|
||||
errorIfNotFound = false,
|
||||
}: {
|
||||
sb: any;
|
||||
id: string;
|
||||
orgId: string;
|
||||
env: string;
|
||||
errorIfNotFound?: boolean;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_triggers")
|
||||
.select()
|
||||
.eq("id", id)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.single();
|
||||
.eq("env", env);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
if (errorIfNotFound) {
|
||||
throw new RecaseError({
|
||||
message: "Referral not found",
|
||||
code: ErrCode.ReferralNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
static async getAll({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: any;
|
||||
orgId: string;
|
||||
env: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_triggers")
|
||||
.select()
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
@@ -49,6 +97,34 @@ export class RewardTriggerService {
|
||||
return insertedData;
|
||||
}
|
||||
|
||||
static async deleteById({
|
||||
sb,
|
||||
id,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: any;
|
||||
id: string;
|
||||
orgId: string;
|
||||
env: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_triggers")
|
||||
.delete()
|
||||
.eq("id", id)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// REFERRAL CODE FUNCTIONS
|
||||
static async getReferralCode({
|
||||
sb,
|
||||
orgId,
|
||||
@@ -65,7 +141,9 @@ export class RewardTriggerService {
|
||||
const { data, error } = await sb
|
||||
.from("referral_codes")
|
||||
.select(
|
||||
withRewardTrigger ? "*, reward_trigger:reward_triggers!inner(*)" : "*"
|
||||
withRewardTrigger
|
||||
? "*, reward_trigger:reward_triggers!inner(*, reward:rewards!inner(*))"
|
||||
: "*"
|
||||
)
|
||||
.eq("code", code)
|
||||
.eq("org_id", orgId)
|
||||
@@ -79,6 +157,38 @@ export class RewardTriggerService {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getCodeByCustomerAndRewardTrigger({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
internalCustomerId,
|
||||
internalRewardTriggerId,
|
||||
}: {
|
||||
sb: any;
|
||||
orgId: string;
|
||||
env: string;
|
||||
internalCustomerId: string;
|
||||
internalRewardTriggerId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("referral_codes")
|
||||
.select("*")
|
||||
.eq("internal_customer_id", internalCustomerId)
|
||||
.eq("internal_reward_trigger_id", internalRewardTriggerId)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
static async createReferralCode({
|
||||
sb,
|
||||
data,
|
||||
@@ -101,21 +211,16 @@ export class RewardTriggerService {
|
||||
|
||||
static async getCodeRedemptionCount({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
code,
|
||||
referralCodeId,
|
||||
}: {
|
||||
sb: any;
|
||||
orgId: string;
|
||||
env: string;
|
||||
code: string;
|
||||
referralCodeId: string;
|
||||
}) {
|
||||
const { data, error, count } = await sb
|
||||
.from("reward_redemptions")
|
||||
.select("*, reward_trigger:reward_triggers!inner(*)", { count: "exact" })
|
||||
.eq("code", code)
|
||||
.eq("reward_trigger.org_id", orgId)
|
||||
.eq("reward_trigger.env", env);
|
||||
.eq("referral_code_id", referralCodeId)
|
||||
.eq("triggered", true);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
import {
|
||||
AppEnv,
|
||||
ReferralCode,
|
||||
Reward,
|
||||
RewardRedemption,
|
||||
RewardTrigger,
|
||||
} from "@autumn/shared";
|
||||
import { RewardTriggerService } from "./RewardTriggerService.js";
|
||||
import { CusService } from "../customers/CusService.js";
|
||||
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import Stripe from "stripe";
|
||||
import { RewardRedemptionService } from "./RewardRedemptionService.js";
|
||||
|
||||
export const generateReferralCode = () => {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const codeLength = 6;
|
||||
@@ -10,3 +24,72 @@ export const generateReferralCode = () => {
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
// Trigger reward
|
||||
export const triggerRedemption = async ({
|
||||
sb,
|
||||
referralCode,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
reward,
|
||||
redemption,
|
||||
}: {
|
||||
sb: any;
|
||||
org: any;
|
||||
env: AppEnv;
|
||||
logger: any;
|
||||
referralCode: ReferralCode;
|
||||
reward: Reward;
|
||||
redemption: RewardRedemption;
|
||||
}) => {
|
||||
logger.info(
|
||||
`Triggering redemption ${redemption.id} for referral code ${referralCode.code}`
|
||||
);
|
||||
|
||||
let applyToCustomer = await CusService.getByInternalId({
|
||||
sb,
|
||||
internalId: referralCode.internal_customer_id,
|
||||
});
|
||||
|
||||
let stripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
await createStripeCusIfNotExists({
|
||||
sb,
|
||||
customer: applyToCustomer,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
let stripeCusId = applyToCustomer.processor.id;
|
||||
let stripeCus = (await stripeCli.customers.retrieve(
|
||||
stripeCusId
|
||||
)) as Stripe.Customer;
|
||||
|
||||
let applied = false;
|
||||
if (!stripeCus.discount) {
|
||||
await stripeCli.customers.update(stripeCusId, {
|
||||
coupon: reward.internal_id,
|
||||
});
|
||||
|
||||
applied = true;
|
||||
logger.info(`Applied coupon to customer in Stripe`);
|
||||
}
|
||||
|
||||
let updatedRedemption = await RewardRedemptionService.update({
|
||||
sb,
|
||||
id: redemption.id,
|
||||
updates: {
|
||||
applied,
|
||||
triggered: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Successfully triggered redemption, applied: ${applied}`);
|
||||
|
||||
return updatedRedemption;
|
||||
};
|
||||
|
||||
92
server/src/internal/rewards/triggerCheckoutReward.ts
Normal file
92
server/src/internal/rewards/triggerCheckoutReward.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { RewardRedemptionService } from "./RewardRedemptionService.js";
|
||||
import { RewardTriggerService } from "./RewardTriggerService.js";
|
||||
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { CusService } from "../customers/CusService.js";
|
||||
import Stripe from "stripe";
|
||||
import { RewardTriggerEvent } from "@autumn/shared";
|
||||
import { triggerRedemption } from "./referralUtils.js";
|
||||
|
||||
export const runTriggerCheckoutReward = async ({
|
||||
sb,
|
||||
payload,
|
||||
logger,
|
||||
}: {
|
||||
sb: any;
|
||||
payload: any;
|
||||
logger: any;
|
||||
}) => {
|
||||
try {
|
||||
// Customer redeeming code, product they're buying
|
||||
let { customer, product, org, env } = payload;
|
||||
|
||||
// 1. Check if redemption exists
|
||||
let redemptions = await RewardRedemptionService.getByCustomer({
|
||||
sb,
|
||||
internalCustomerId: customer.internal_id, // customer that redeemed code
|
||||
withRewardTrigger: true,
|
||||
triggered: false,
|
||||
withReferralCode: true,
|
||||
triggerWhen: RewardTriggerEvent.Checkout,
|
||||
});
|
||||
|
||||
for (let redemption of redemptions) {
|
||||
if (
|
||||
!redemption ||
|
||||
redemption.reward_trigger.when !== RewardTriggerEvent.Checkout
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { reward_trigger, referral_code: referralCode } = redemption;
|
||||
let { reward } = reward_trigger;
|
||||
|
||||
logger.info(`--------------------------------`);
|
||||
logger.info(`CHECKING FOR CHECKOUT REWARD, ORG: ${org.slug}`);
|
||||
logger.info(
|
||||
`Redeemed by: ${customer.name} (${customer.id}) for referral program: ${reward_trigger.id}`
|
||||
);
|
||||
logger.info(`Referral code: ${referralCode.code} (${referralCode.id})`);
|
||||
|
||||
if (!reward_trigger.product_ids.includes(product.id)) {
|
||||
logger.info(
|
||||
`Product ${product.name} (${product.id}) not included in referral program, skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get redemption count
|
||||
let redemptionCount = await RewardTriggerService.getCodeRedemptionCount({
|
||||
sb,
|
||||
referralCodeId: referralCode.id,
|
||||
});
|
||||
|
||||
if (redemptionCount >= reward_trigger.max_redemptions) {
|
||||
logger.info(
|
||||
`Max redemptions reached, not triggering latest redemption`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await triggerRedemption({
|
||||
sb,
|
||||
referralCode,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
reward,
|
||||
redemption,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to trigger checkout reward");
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
// let { reward_trigger } = redemption;
|
||||
// let { reward } = reward_trigger;
|
||||
|
||||
// let customerToApplyDiscount = redemption.code.internal_customer_id;
|
||||
|
||||
// console.log("Customer to apply discount", customerToApplyDiscount);
|
||||
};
|
||||
@@ -3,4 +3,6 @@ export enum JobName {
|
||||
UpdateUsage = "update-usage",
|
||||
|
||||
Migration = "migration",
|
||||
|
||||
TriggerCheckoutReward = "trigger-checkout-reward",
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { JobName } from "./JobName.js";
|
||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { runMigrationTask } from "@/internal/migrations/runMigrationTask.js";
|
||||
import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js";
|
||||
|
||||
const NUM_WORKERS = 5;
|
||||
|
||||
@@ -83,6 +84,16 @@ const initWorker = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.name == JobName.TriggerCheckoutReward) {
|
||||
await runTriggerCheckoutReward({
|
||||
payload: job.data,
|
||||
sb,
|
||||
logger: logtail,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { customerId } = job.data;
|
||||
|
||||
while (!(await acquireLock({ customerId, timeout: 10000, useBackup }))) {
|
||||
|
||||
@@ -113,4 +113,7 @@ export const ErrCode = {
|
||||
|
||||
// Referral codes
|
||||
ReferralCodeMaxRedemptionsReached: "referral_code_max_redemptions_reached",
|
||||
ReferralNotFound: "referral_not_found",
|
||||
CustomerAlreadyRedeemedReferralCode:
|
||||
"customer_already_redeemed_referral_code",
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from "./models/productModels/fixedPriceModels.js";
|
||||
export * from "./models/productModels/freeTrialModels.js";
|
||||
export * from "./models/rewardModels/rewardModels.js";
|
||||
export * from "./models/rewardModels/rewardTriggerModels.js";
|
||||
export * from "./models/rewardModels/referralModels/referralModels.js";
|
||||
// Gen Models
|
||||
export * from "./models/genModels.js";
|
||||
|
||||
|
||||
@@ -16,17 +16,22 @@ let ReferralCodeSchema = z.object({
|
||||
|
||||
let RewardRedemptionSchema = z.object({
|
||||
id: z.string(),
|
||||
internal_reward_trigger_id: z.string().nullish(),
|
||||
created_at: z.number(),
|
||||
updated_at: z.number(),
|
||||
|
||||
// Customer who signed up / paid
|
||||
internal_customer_id: z.string(), // customer who redeemed the code...
|
||||
internal_reward_trigger_id: z.string(), // reward trigger that was redeemed
|
||||
|
||||
// Referral code used
|
||||
code: z.string(),
|
||||
// code: z.string(),
|
||||
referral_code_id: z.string(),
|
||||
|
||||
// Whether the reward was triggered
|
||||
triggered: z.boolean(),
|
||||
|
||||
// Whether the (coupon) was applied
|
||||
applied: z.boolean(),
|
||||
});
|
||||
|
||||
export type ReferralCode = z.infer<typeof ReferralCodeSchema>;
|
||||
|
||||
@@ -40,3 +40,4 @@ export const CreateCouponSchema = CouponSchema.omit({
|
||||
|
||||
export type Coupon = z.infer<typeof CouponSchema>;
|
||||
export type CreateCoupon = z.infer<typeof CreateCouponSchema>;
|
||||
export type Reward = z.infer<typeof CouponSchema>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import z from "zod";
|
||||
import { Reward } from "../rewardModels/rewardModels.js";
|
||||
|
||||
export enum RewardTriggerEvent {
|
||||
SignUp = "sign_up",
|
||||
// SignUp = "sign_up",
|
||||
Immediately = "immediately",
|
||||
Checkout = "checkout",
|
||||
}
|
||||
|
||||
@@ -9,10 +11,9 @@ export const RewardTrigger = z.object({
|
||||
internal_id: z.string(),
|
||||
id: z.string(),
|
||||
|
||||
trigger: z.object({
|
||||
type: z.nativeEnum(RewardTriggerEvent),
|
||||
product_id: z.string().optional(),
|
||||
}),
|
||||
when: z.nativeEnum(RewardTriggerEvent),
|
||||
product_ids: z.array(z.string()).optional(),
|
||||
exclude_trial: z.boolean().optional(),
|
||||
|
||||
internal_reward_id: z.string(),
|
||||
|
||||
@@ -26,13 +27,16 @@ export const RewardTrigger = z.object({
|
||||
|
||||
export const CreateRewardTrigger = z.object({
|
||||
id: z.string(),
|
||||
trigger: z.object({
|
||||
type: z.nativeEnum(RewardTriggerEvent),
|
||||
product_id: z.string().optional(),
|
||||
}),
|
||||
when: z.nativeEnum(RewardTriggerEvent),
|
||||
product_ids: z.array(z.string()).optional(),
|
||||
exclude_trial: z.boolean().optional(),
|
||||
internal_reward_id: z.string(),
|
||||
max_redemptions: z.number().optional(),
|
||||
});
|
||||
|
||||
export type RewardTrigger = z.infer<typeof RewardTrigger>;
|
||||
export type CreateRewardTrigger = z.infer<typeof CreateRewardTrigger>;
|
||||
|
||||
export type FullRewardTrigger = RewardTrigger & {
|
||||
reward: Reward;
|
||||
};
|
||||
|
||||
@@ -54,7 +54,9 @@ export const CouponConfig = ({
|
||||
Promotional Code
|
||||
</FieldLabel>
|
||||
<Input
|
||||
value={coupon.promo_codes.length > 0 ? coupon.promo_codes[0].code : ""}
|
||||
value={
|
||||
coupon.promo_codes.length > 0 ? coupon.promo_codes[0].code : ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setCoupon({
|
||||
...coupon,
|
||||
@@ -293,11 +295,3 @@ const ProductPriceSelector = ({
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
// productName: string;
|
||||
// prices: { value: string; label: string }[];
|
||||
// }
|
||||
// ][]
|
||||
// ).map(([productId, productData]) => (
|
||||
|
||||
// ))} */}
|
||||
|
||||
78
vite/src/views/products/product/ConfirmMigrateDialog.tsx
Normal file
78
vite/src/views/products/product/ConfirmMigrateDialog.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogHeader,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useProductContext } from "./ProductContext";
|
||||
import { toast } from "sonner";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ConfirmNewVersionDialog({
|
||||
open,
|
||||
setOpen,
|
||||
startMigration,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
startMigration: () => Promise<void>;
|
||||
}) {
|
||||
const { product, version } = useProductContext();
|
||||
let [confirmText, setConfirmText] = useState("");
|
||||
let [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClick = async () => {
|
||||
if (confirmText !== product.id) {
|
||||
toast.error("Confirmation text is incorrect");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
await startMigration();
|
||||
setIsLoading(false);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{/* <Button>Confirm New Version</Button> */}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Migrate customers?</DialogTitle>
|
||||
<DialogDescription className="text-sm flex flex-col gap-4">
|
||||
<p>
|
||||
Note: This will migrate all customers on {product.name} (version{" "}
|
||||
{version}) to the latest version.
|
||||
</p>
|
||||
<p>
|
||||
Type <code className="font-bold">{product.id}</code> to continue.
|
||||
</p>
|
||||
<Input
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
type="text"
|
||||
placeholder={product.id}
|
||||
className="w-full text-black"
|
||||
/>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="gradientPrimary"
|
||||
onClick={onClick}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Start migration
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { pricesOnlyOneOff } from "@/utils/product/priceUtils";
|
||||
import ConfirmMigrateDialog from "./ConfirmMigrateDialog";
|
||||
|
||||
export const ManageProduct = ({
|
||||
product,
|
||||
@@ -197,8 +198,9 @@ const CountAndMigrate = () => {
|
||||
let env = useEnv();
|
||||
let axiosInstance = useAxiosInstance({ env });
|
||||
let [loading, setLoading] = useState(false);
|
||||
let [confirmMigrateOpen, setConfirmMigrateOpen] = useState(false);
|
||||
|
||||
const onMigrateClicked = async () => {
|
||||
const migrateCustomers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let { data } = await axiosInstance.post("/v1/migrations", {
|
||||
@@ -216,6 +218,10 @@ const CountAndMigrate = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onMigrateClicked = () => {
|
||||
setConfirmMigrateOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (existingMigrations.length > 0) {
|
||||
// Run poll job on mutate
|
||||
@@ -305,6 +311,11 @@ const CountAndMigrate = () => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<ConfirmMigrateDialog
|
||||
open={confirmMigrateOpen}
|
||||
setOpen={setConfirmMigrateOpen}
|
||||
startMigration={migrateCustomers}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -35,10 +35,14 @@ import { CreateRewardTrigger } from "@autumn/shared";
|
||||
|
||||
const defaultRewardTrigger: CreateRewardTrigger = {
|
||||
id: "",
|
||||
trigger: {
|
||||
type: RewardTriggerEvent.SignUp,
|
||||
product_id: "",
|
||||
},
|
||||
// trigger: {
|
||||
// type: RewardTriggerEvent.SignUp,
|
||||
// product_ids: [],
|
||||
// exclude_trial: false,
|
||||
// },
|
||||
when: RewardTriggerEvent.Immediately,
|
||||
product_ids: [],
|
||||
exclude_trial: false,
|
||||
internal_reward_id: "",
|
||||
max_redemptions: 0,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,24 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { RewardTrigger, Coupon, RewardTriggerEvent } from "@autumn/shared";
|
||||
import { useProductsContext } from "../ProductsContext";
|
||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, ChevronsUpDown, X } from "lucide-react";
|
||||
|
||||
export const RewardTriggerConfig = ({
|
||||
rewardTrigger,
|
||||
@@ -18,6 +36,7 @@ export const RewardTriggerConfig = ({
|
||||
setRewardTrigger: (rewardTrigger: RewardTrigger) => void;
|
||||
}) => {
|
||||
let { coupons } = useProductsContext();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -30,19 +49,6 @@ export const RewardTriggerConfig = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-6/12">
|
||||
<FieldLabel>Redeem On</FieldLabel>
|
||||
<Select defaultValue={RewardTriggerEvent.SignUp}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a redeem on" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={RewardTriggerEvent.SignUp}>Sign Up</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6/12">
|
||||
<FieldLabel>Coupon</FieldLabel>
|
||||
<Select
|
||||
@@ -63,10 +69,37 @@ export const RewardTriggerConfig = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6/12">
|
||||
<FieldLabel>Redeem On</FieldLabel>
|
||||
<Select
|
||||
defaultValue={RewardTriggerEvent.Immediately}
|
||||
value={rewardTrigger.when}
|
||||
onValueChange={(value) =>
|
||||
setRewardTrigger({
|
||||
...rewardTrigger,
|
||||
when: value as RewardTriggerEvent,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a redeem on" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(RewardTriggerEvent).map((event) => (
|
||||
<SelectItem key={event} value={event}>
|
||||
{keyToTitle(event)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-6/12">
|
||||
<FieldLabel>Max Redemptions</FieldLabel>
|
||||
<Input
|
||||
value={rewardTrigger.max_redemptions || ""}
|
||||
type="number"
|
||||
value={rewardTrigger.max_redemptions}
|
||||
onChange={(e) =>
|
||||
setRewardTrigger({
|
||||
...rewardTrigger,
|
||||
@@ -74,24 +107,118 @@ export const RewardTriggerConfig = ({
|
||||
})
|
||||
}
|
||||
/>
|
||||
{/* Add infinity */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{rewardTrigger.when === RewardTriggerEvent.Checkout && (
|
||||
<div className="w-full">
|
||||
<FieldLabel>Products</FieldLabel>
|
||||
<ProductSelector
|
||||
rewardTrigger={rewardTrigger}
|
||||
setRewardTrigger={setRewardTrigger}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
/* <FieldLabel description="How users redeem the coupon">
|
||||
Promotional Code
|
||||
</FieldLabel>
|
||||
<Input
|
||||
value={coupon.promo_codes.length > 0 ? coupon.promo_codes[0].code : ""}
|
||||
onChange={(e) =>
|
||||
setCoupon({
|
||||
...coupon,
|
||||
promo_codes: [{ code: e.target.value }],
|
||||
})
|
||||
}
|
||||
/> */
|
||||
}
|
||||
const ProductSelector = ({
|
||||
rewardTrigger,
|
||||
setRewardTrigger,
|
||||
}: {
|
||||
rewardTrigger: RewardTrigger;
|
||||
setRewardTrigger: (rewardTrigger: RewardTrigger) => void;
|
||||
}) => {
|
||||
const { products } = useProductsContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Handle selection/deselection of a product
|
||||
const handleProductToggle = (productId: string) => {
|
||||
let newProductIds = [...(rewardTrigger.product_ids || [])];
|
||||
if (newProductIds.includes(productId)) {
|
||||
newProductIds = newProductIds.filter((id) => id !== productId);
|
||||
} else {
|
||||
newProductIds = [...newProductIds, productId];
|
||||
}
|
||||
setRewardTrigger({
|
||||
...rewardTrigger,
|
||||
product_ids: newProductIds,
|
||||
});
|
||||
};
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
return <p className="text-sm text-t3">No products available</p>;
|
||||
}
|
||||
|
||||
const getProductText = (productId: string) => {
|
||||
const product = products.find((p: any) => p.id === productId);
|
||||
return product?.name || "Unknown Product";
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between min-h-9 flex flex-wrap h-fit py-2 justify-start items-center gap-2 relative hover:bg-zinc-50"
|
||||
>
|
||||
{rewardTrigger.product_ids?.length === 0 ? (
|
||||
"Select Products"
|
||||
) : (
|
||||
<>
|
||||
{rewardTrigger.product_ids?.map((productId) => (
|
||||
<div
|
||||
key={productId}
|
||||
className="py-0 px-3 text-xs text-t3 border-zinc-300 bg-zinc-100 rounded-full w-fit flex items-center gap-2 h-fit"
|
||||
>
|
||||
<p className="text-t2">{getProductText(productId)}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleProductToggle(productId);
|
||||
}}
|
||||
className="bg-transparent hover:bg-transparent p-0 w-5 h-5"
|
||||
>
|
||||
<X size={12} className="text-t3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50 absolute right-2" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search products..." className="h-9" />
|
||||
<CommandList className="max-h-[300px] overflow-y-auto">
|
||||
<ScrollArea>
|
||||
<CommandEmpty>No products found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{products.map((product: any) => (
|
||||
<CommandItem
|
||||
key={product.id}
|
||||
value={product.id}
|
||||
onSelect={() => handleProductToggle(product.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center">{product.name}</div>
|
||||
{rewardTrigger.product_ids?.includes(product.id) && (
|
||||
<Check size={12} className="text-t3" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from "react";
|
||||
import { useProductsContext } from "../ProductsContext";
|
||||
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { RewardTrigger } from "@autumn/shared";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
import { toast } from "sonner";
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { ToolbarButton } from "@/components/general/table-components/ToolbarButton";
|
||||
import { Delete } from "lucide-react";
|
||||
|
||||
export const RewardTriggerRowToolbar = ({
|
||||
rewardTrigger,
|
||||
}: {
|
||||
rewardTrigger: RewardTrigger;
|
||||
}) => {
|
||||
const { env, mutate } = useProductsContext();
|
||||
const axiosInstance = useAxiosInstance({ env });
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setDeleteLoading(true);
|
||||
|
||||
try {
|
||||
await axiosInstance.delete(`/v1/reward-triggers/${rewardTrigger.id}`);
|
||||
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
toast.error(getBackendErr(error, "Failed to delete reward trigger"));
|
||||
}
|
||||
|
||||
setDeleteLoading(false);
|
||||
setDeleteOpen(false);
|
||||
};
|
||||
return (
|
||||
<DropdownMenu open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ToolbarButton />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="text-t2">
|
||||
<DropdownMenuItem
|
||||
className="flex items-center"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await handleDelete();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full gap-2">
|
||||
Delete
|
||||
{deleteLoading ? (
|
||||
<SmallSpinner />
|
||||
) : (
|
||||
<Delete size={12} className="text-t3" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -20,14 +20,15 @@ import { TableBody } from "@/components/ui/table";
|
||||
|
||||
import { Table, TableHead, TableRow, TableHeader } from "@/components/ui/table";
|
||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { RewardTriggerRowToolbar } from "./RewardTriggerRowToolbar";
|
||||
|
||||
export const RewardTriggersTable = () => {
|
||||
const { org } = useProductsContext();
|
||||
const { rewardTriggers } = useProductsContext();
|
||||
const [selectedRewardTrigger, setSelectedRewardTrigger] =
|
||||
useState<RewardTrigger | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
let rewardTriggers: RewardTrigger[] = [];
|
||||
// let rewardTriggers: RewardTrigger[] = [];
|
||||
// const handleRowClick = (id: string) => {
|
||||
// const creditSystem = creditSystems.find(
|
||||
// (creditSystem: Feature) => creditSystem.id === id
|
||||
@@ -50,60 +51,61 @@ export const RewardTriggersTable = () => {
|
||||
<Table>
|
||||
<TableHeader className="rounded-full">
|
||||
<TableRow>
|
||||
<TableHead className="">Name</TableHead>
|
||||
<TableHead>Promo Codes</TableHead>
|
||||
<TableHead>Discount</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead className="">ID</TableHead>
|
||||
<TableHead>Redeem On</TableHead>
|
||||
<TableHead>Max Redemptions</TableHead>
|
||||
<TableHead>Products</TableHead>
|
||||
|
||||
<TableHead className="min-w-0 w-28">Created At</TableHead>
|
||||
<TableHead className="min-w-0 w-10"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rewardTriggers.map((rewardTrigger: RewardTrigger) => (
|
||||
<TableRow
|
||||
key={rewardTrigger.id}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedRewardTrigger(rewardTrigger);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
{rewardTrigger.name}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono">
|
||||
{rewardTrigger.trigger.type}
|
||||
</TableCell>
|
||||
<TableCell className="min-w-32">
|
||||
<div className="flex items-center gap-1">
|
||||
<p>{rewardTrigger.max_redemptions} </p>
|
||||
<p className="text-t3">
|
||||
{rewardTrigger.unlimited_redemptions
|
||||
? "Unlimited"
|
||||
: rewardTrigger.max_redemptions}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
{rewardTrigger.trigger.type == RewardTriggerEvent.SignUp
|
||||
? "Sign Up"
|
||||
: rewardTrigger.trigger.type == RewardTriggerEvent.Checkout
|
||||
? "Checkout"
|
||||
: keyToTitle(rewardTrigger.trigger.type)}
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
{formatUnixToDateTime(rewardTrigger.created_at).date}
|
||||
<span className="text-t3">
|
||||
{" "}
|
||||
{formatUnixToDateTime(rewardTrigger.created_at).time}{" "}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
{/* <RewardTriggerRowToolbar rewardTrigger={rewardTrigger} /> */}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{rewardTriggers.map((rewardTrigger: RewardTrigger) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={rewardTrigger.id}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedRewardTrigger(rewardTrigger);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<TableCell className="font-medium font-mono">
|
||||
{rewardTrigger.id}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono">
|
||||
{rewardTrigger.when}
|
||||
</TableCell>
|
||||
<TableCell className="min-w-32">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-t3">
|
||||
{rewardTrigger.unlimited_redemptions
|
||||
? "Unlimited"
|
||||
: rewardTrigger.max_redemptions}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
{rewardTrigger.when == RewardTriggerEvent.Immediately
|
||||
? "Sign Up"
|
||||
: rewardTrigger.when == RewardTriggerEvent.Checkout
|
||||
? "Checkout"
|
||||
: keyToTitle(rewardTrigger.when)}
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
{formatUnixToDateTime(rewardTrigger.created_at).date}
|
||||
<span className="text-t3">
|
||||
{" "}
|
||||
{formatUnixToDateTime(rewardTrigger.created_at).time}{" "}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<RewardTriggerRowToolbar rewardTrigger={rewardTrigger} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user