moving to qa referrals

This commit is contained in:
John Yeo
2025-04-09 10:45:08 +01:00
parent fd790c2ef4
commit 44b459d452
27 changed files with 1200 additions and 304 deletions

View File

@@ -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({

View 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}`);
}

View File

@@ -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();

View File

@@ -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" });
// }
// });
// });

View File

@@ -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 };

View 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);
},
})
);

View File

@@ -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);
},
})
);

View File

@@ -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,

View File

@@ -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);

View 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;
}
}

View File

@@ -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;

View File

@@ -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;
};

View 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);
};

View File

@@ -3,4 +3,6 @@ export enum JobName {
UpdateUsage = "update-usage",
Migration = "migration",
TriggerCheckoutReward = "trigger-checkout-reward",
}

View File

@@ -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 }))) {

View File

@@ -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",
};

View File

@@ -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";

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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;
};

View File

@@ -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]) => (
// ))} */}

View 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>
);
}

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
</>