fix: redeem free product on customer creation
This commit is contained in:
@@ -10,7 +10,7 @@ $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
|
||||
'tests/attach/updateQuantity/*.ts' \
|
||||
'tests/advanced/referrals/*.ts'
|
||||
|
||||
$MOCHA_CMD 'tests/attach/multiProduct/*.ts'
|
||||
# $MOCHA_CMD 'tests/attach/multiProduct/*.ts'
|
||||
|
||||
$MOCHA_CMD 'tests/advanced/usage/*.ts'
|
||||
# $MOCHA_CMD 'tests/advanced/usage/*.ts'
|
||||
|
||||
@@ -117,17 +117,6 @@ export const handleProductsUpdated = async ({
|
||||
deletedCusProduct,
|
||||
} = data;
|
||||
|
||||
if (!req) {
|
||||
logger.warn("products.updated, no req object found, skipping", {
|
||||
...data,
|
||||
org: {
|
||||
id: org.id,
|
||||
slug: org.slug,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Product:
|
||||
let product = cusProduct.product;
|
||||
let prices = cusProduct.customer_prices.map((cp) => cp.price);
|
||||
@@ -181,26 +170,32 @@ export const handleProductsUpdated = async ({
|
||||
// 1. Log action to DB
|
||||
|
||||
try {
|
||||
let action = constructAction({
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
entity: customer.entity,
|
||||
type: ActionType.CustomerProductsUpdated,
|
||||
req,
|
||||
properties: {
|
||||
product_id: product.id,
|
||||
customer_product_id: cusProduct.id,
|
||||
scenario,
|
||||
if (req) {
|
||||
let action = constructAction({
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
entity: customer.entity,
|
||||
type: ActionType.CustomerProductsUpdated,
|
||||
req,
|
||||
properties: {
|
||||
product_id: product.id,
|
||||
customer_product_id: cusProduct.id,
|
||||
scenario,
|
||||
|
||||
deleted_product_id: deletedCusProduct?.product.id,
|
||||
scheduled_product_id: scheduledCusProduct?.product.id,
|
||||
deleted_product_id: deletedCusProduct?.product.id,
|
||||
scheduled_product_id: scheduledCusProduct?.product.id,
|
||||
|
||||
body: req.body,
|
||||
},
|
||||
});
|
||||
body: req.body,
|
||||
},
|
||||
});
|
||||
|
||||
await ActionService.insert(db, action);
|
||||
await ActionService.insert(db, action);
|
||||
} else {
|
||||
logger.warn(
|
||||
"products.updated, no req object found, skipping action insert",
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Failed to log action to DB", {
|
||||
message: error.message,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import {
|
||||
generateReferralCode,
|
||||
triggerFreeProduct,
|
||||
triggerRedemption,
|
||||
} from "@/internal/rewards/referralUtils.js";
|
||||
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
||||
@@ -8,11 +9,15 @@ import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionServ
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { generateId, notNullish } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { ErrCode, RewardTriggerEvent } from "@autumn/shared";
|
||||
import { ErrCode, RewardCategory, RewardTriggerEvent } from "@autumn/shared";
|
||||
import express, { Router } from "express";
|
||||
import { RewardRedemption } from "@autumn/shared";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
import { getRewardCat } from "@/internal/rewards/rewardUtils.js";
|
||||
import { RewardService } from "@/internal/rewards/RewardService.js";
|
||||
import { parseReqForAction } from "@/internal/analytics/actionUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export const referralRouter: Router = express.Router();
|
||||
|
||||
@@ -215,15 +220,45 @@ referralRouter.post("/redeem", (req, res) =>
|
||||
if (
|
||||
referralCode.reward_program.when === RewardTriggerEvent.CustomerCreation
|
||||
) {
|
||||
redemption = await triggerRedemption({
|
||||
db: req.db,
|
||||
referralCode,
|
||||
org,
|
||||
const reward = await RewardService.get({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
logger,
|
||||
reward: reward_program.reward,
|
||||
redemption,
|
||||
idOrInternalId: reward_program.internal_reward_id,
|
||||
});
|
||||
|
||||
if (!reward) {
|
||||
throw new RecaseError({
|
||||
message: `Reward ${reward_program.internal_reward_id} not found`,
|
||||
statusCode: 404,
|
||||
code: ErrCode.RewardNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
let rewardCat = getRewardCat(reward);
|
||||
if (rewardCat === RewardCategory.FreeProduct) {
|
||||
await triggerFreeProduct({
|
||||
req: parseReqForAction(req) as ExtendedRequest,
|
||||
db,
|
||||
referralCode,
|
||||
redeemer: customer,
|
||||
rewardProgram: reward_program,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
redemption,
|
||||
});
|
||||
} else {
|
||||
await triggerRedemption({
|
||||
db,
|
||||
referralCode,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
reward,
|
||||
redemption,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add coupon to customer?
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
FullCustomerEntitlement,
|
||||
FullEntitlement,
|
||||
getFeatureInvoiceDescription,
|
||||
InsertReplaceable,
|
||||
PreviewLineItem,
|
||||
Price,
|
||||
} from "@autumn/shared";
|
||||
@@ -156,6 +155,7 @@ export const getContUseInvoiceItems = async ({
|
||||
attachParams,
|
||||
prevCusEnt,
|
||||
});
|
||||
|
||||
const prevItem = curItems.find(
|
||||
(item) => item.price_id === prevCusPrice?.price.id,
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
|
||||
import { Decimal } from "decimal.js";
|
||||
import { intervalsAreSame } from "../attachUtils/getAttachConfig.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
const getNextCycleAt = ({
|
||||
prices,
|
||||
@@ -184,6 +185,8 @@ export const getUpgradeProductPreview = async ({
|
||||
freeTrial: attachParams.freeTrial,
|
||||
});
|
||||
|
||||
items = items.filter((item) => item.amount !== 0);
|
||||
|
||||
if (branch == AttachBranch.UpdatePrepaidQuantity) {
|
||||
items = items.filter((item) => item.usage_model == UsageModel.Prepaid);
|
||||
dueNextCycle!.line_items = dueNextCycle!.line_items.filter(
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
subIsPrematurelyCanceled,
|
||||
} from "@/external/stripe/stripeSubUtils.js";
|
||||
import { getRelatedCusEnt } from "./cusPrices/cusPriceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { BREAK_API_VERSION } from "@/utils/constants.js";
|
||||
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
@@ -553,5 +553,5 @@ export const getFeatureQuantity = ({
|
||||
const option = options.find(
|
||||
(o) => o.internal_feature_id == internalFeatureId,
|
||||
);
|
||||
return option?.quantity || 1;
|
||||
return nullish(option?.quantity) ? 1 : option?.quantity!;
|
||||
};
|
||||
|
||||
@@ -157,6 +157,7 @@ export const priceToInvoiceDescription = ({
|
||||
cusProduct,
|
||||
internalFeatureId: ent.feature.internal_id,
|
||||
});
|
||||
|
||||
description = formatPrepaidPrice({ price, ents, quantity });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FullRewardProgram,
|
||||
ReferralCode,
|
||||
Reward,
|
||||
RewardCategory,
|
||||
RewardReceivedBy,
|
||||
RewardRedemption,
|
||||
} from "@autumn/shared";
|
||||
@@ -22,6 +23,8 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusProductService } from "../customers/cusProducts/CusProductService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { getRewardCat } from "./rewardUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export const generateReferralCode = () => {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
@@ -114,6 +117,7 @@ export const triggerRedemption = async ({
|
||||
};
|
||||
|
||||
export const triggerFreeProduct = async ({
|
||||
req,
|
||||
db,
|
||||
referralCode,
|
||||
redeemer,
|
||||
@@ -123,6 +127,7 @@ export const triggerFreeProduct = async ({
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
req?: ExtendedRequest;
|
||||
db: DrizzleCli;
|
||||
referralCode: ReferralCode;
|
||||
redeemer: Customer;
|
||||
@@ -177,6 +182,7 @@ export const triggerFreeProduct = async ({
|
||||
]);
|
||||
|
||||
let attachParams: InsertCusProductParams = {
|
||||
req,
|
||||
org,
|
||||
product: fullProduct,
|
||||
prices: fullProduct.prices,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { db } from "@/db/initDrizzle.js";
|
||||
import sendOTPEmail from "@/internal/emails/sendOTPEmail.js";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
@@ -48,7 +49,7 @@ export const auth = betterAuth({
|
||||
"http://localhost:3000",
|
||||
"https://app.useautumn.com",
|
||||
"https://*.useautumn.com",
|
||||
process.env.CLIENT_URL!,
|
||||
// process.env.CLIENT_URL!,
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
|
||||
@@ -18,8 +18,8 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
|
||||
const testCase = "updateQuantity1";
|
||||
|
||||
@@ -111,33 +111,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid singl
|
||||
});
|
||||
});
|
||||
|
||||
// const newOpts = [
|
||||
// {
|
||||
// feature_id: TestFeature.Users,
|
||||
// quantity: 1,
|
||||
// },
|
||||
// ];
|
||||
// it("should throw error if try to reduce seats to less than current usage", async function () {
|
||||
// await autumn.track({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Users,
|
||||
// value: 2,
|
||||
// });
|
||||
|
||||
// await timeout(1000);
|
||||
|
||||
// await expectAutumnError({
|
||||
// errCode: AttachErrCode.InvalidOptions,
|
||||
// func: async () => {
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: pro.id,
|
||||
// options: newOpts,
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
const updatedOpts = [
|
||||
{
|
||||
feature_id: TestFeature.Users,
|
||||
@@ -146,6 +119,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid singl
|
||||
];
|
||||
|
||||
it("should update quantity to 4 users and have usage stay the same", async function () {
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 2,
|
||||
});
|
||||
await timeout(3000);
|
||||
|
||||
curUnix = await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
|
||||
Reference in New Issue
Block a user