fix: connect stripe deleted clerk functions, allow reward to be passed into attach body
This commit is contained in:
12
.devcontainer/devcontainer.json
Normal file
12
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Autumn dev container",
|
||||
"image": "mcr.microsoft.com/devcontainers/node:18",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"forwardPorts": [8080],
|
||||
// "postCreateCommand": "pnpm install",
|
||||
"postCreateCommand": "apt update && apt install -y zsh",
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/zsh"
|
||||
},
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
@@ -7,10 +7,10 @@ docker compose -f docker-compose.db.yml up --build
|
||||
# Create DB tables
|
||||
pnpm
|
||||
|
||||
# docker volume rm main-repo_shared-node-modules main-repo_root-node-modules main-repo_vite-node-modules
|
||||
# docker volume rm autumn-oss_shared-node-modules autumn-oss_root-node-modules autumn-oss_vite-node-modules
|
||||
# Dev
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
docker volume rm autumn-oss_shared-node-modules autumn-oss_root-node-modules autumn-oss_vite-node-modules
|
||||
docker volume rm main-repo_shared-node-modules main-repo_root-node-modules main-repo_vite-node-modules
|
||||
docker compose -f docker-compose.dev.yml build --no-cache
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
|
||||
|
||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -67,8 +67,8 @@ importers:
|
||||
specifier: ^4.3.10
|
||||
version: 4.3.16(react@18.3.1)(zod@3.25.64)
|
||||
autumn-js:
|
||||
specifier: ^0.0.64
|
||||
version: 0.0.64(@tanstack/react-query@5.80.7(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^0.0.65
|
||||
version: 0.0.65(@tanstack/react-query@5.80.7(react@18.3.1))(react@18.3.1)
|
||||
axios:
|
||||
specifier: ^1.8.3
|
||||
version: 1.10.0
|
||||
@@ -260,6 +260,9 @@ importers:
|
||||
specifier: ^3.25.23
|
||||
version: 3.25.64
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.0.3
|
||||
version: 24.0.3
|
||||
nodemon:
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.10
|
||||
@@ -2685,8 +2688,8 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
autumn-js@0.0.64:
|
||||
resolution: {integrity: sha512-Fa5lr9A0ywYNcbny/dQBRKSGaqnTuUvqtiBegHrr5Z3wCw9A/Z2LRH/f8AqHYQSFOXVkS763ngLlGLxfJpYgQQ==}
|
||||
autumn-js@0.0.65:
|
||||
resolution: {integrity: sha512-YKR5kNDdpYvxGDNEmgqtqrDODyJG3frApR2HC9z05hazg94rEDSoatfUh7iweJUAGSauhqhjWADtgMvRRbSLfA==}
|
||||
peerDependencies:
|
||||
'@tanstack/react-query': ^5.76.1
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
@@ -7829,7 +7832,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
autumn-js@0.0.64(@tanstack/react-query@5.80.7(react@18.3.1))(react@18.3.1):
|
||||
autumn-js@0.0.65(@tanstack/react-query@5.80.7(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@tanstack/react-query': 5.80.7(react@18.3.1)
|
||||
rou3: 0.6.3
|
||||
|
||||
4
run.sh
4
run.sh
@@ -1,7 +1,11 @@
|
||||
if [[ $1 == *"docker-compose"* ]]; then
|
||||
if [[ $2 == "down" ]]; then
|
||||
docker compose -f "$1" down $3
|
||||
else
|
||||
if [[ $3 == *"--build"* ]]; then
|
||||
docker compose -f "$1" up --build
|
||||
else
|
||||
docker compose -f "$1" up $3
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"ai": "^4.3.10",
|
||||
"autumn-js": "^0.0.64",
|
||||
"autumn-js": "^0.0.65",
|
||||
"axios": "^1.8.3",
|
||||
"better-auth": "^1.2.9",
|
||||
"body-parser": "^1.20.3",
|
||||
|
||||
29
server/src/external/logtail/logtailUtils.ts
vendored
29
server/src/external/logtail/logtailUtils.ts
vendored
@@ -7,16 +7,33 @@ import { Logtail } from "@logtail/node";
|
||||
const pinoLogger = initLogger();
|
||||
|
||||
const createLogMethod = (pinoMethod: any, logtailMethod: any) => {
|
||||
function rewriteAppPath(str: string) {
|
||||
if (typeof str !== "string") return str;
|
||||
return str.replace(/\/app\//g, "./");
|
||||
}
|
||||
|
||||
function rewriteErrorStack(error: Error) {
|
||||
if (error instanceof Error && typeof error.stack === "string") {
|
||||
const newError = new Error(error.message);
|
||||
newError.stack = rewriteAppPath(error.stack);
|
||||
return newError;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
return (...args: any[]) => {
|
||||
let message = "";
|
||||
let mergedObj = {};
|
||||
|
||||
// Helper function to convert Error objects to plain objects
|
||||
|
||||
const strings = args.filter((arg) => typeof arg === "string");
|
||||
const objects = args.filter(
|
||||
(arg) => typeof arg !== "string" && arg !== null,
|
||||
);
|
||||
const strings = args
|
||||
.filter((arg) => typeof arg === "string")
|
||||
.map(rewriteAppPath);
|
||||
const objects = args
|
||||
.filter((arg) => typeof arg !== "string" && arg !== null)
|
||||
.map((obj) => (obj instanceof Error ? rewriteErrorStack(obj) : obj));
|
||||
|
||||
// Use last string as message, or use Error message if no strings provided
|
||||
if (strings.length > 0) {
|
||||
@@ -25,7 +42,9 @@ const createLogMethod = (pinoMethod: any, logtailMethod: any) => {
|
||||
// If no string message but we have an Error object, use its stack trace
|
||||
const errorObject = args.find((arg) => arg instanceof Error);
|
||||
if (errorObject) {
|
||||
message = errorObject.stack || errorObject.message || "Error occurred";
|
||||
message = rewriteAppPath(
|
||||
errorObject.stack || errorObject.message || "Error occurred",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ export const createStripeCoupon = async ({
|
||||
}) => {
|
||||
let discountConfig = reward.discount_config;
|
||||
|
||||
try {
|
||||
await stripeCli.coupons.del(reward.id);
|
||||
} catch (error) {}
|
||||
|
||||
let stripeProdIds = prices.map((price) => {
|
||||
if (price.config!.type === PriceType.Fixed) {
|
||||
return price.product.processor?.id;
|
||||
|
||||
12
server/src/external/stripe/stripeCusUtils.ts
vendored
12
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -41,7 +41,13 @@ export const createStripeCusIfNotExists = async ({
|
||||
createNew = true;
|
||||
} else {
|
||||
try {
|
||||
await stripeCli.customers.retrieve(customer.processor.id);
|
||||
let stripeCus = await stripeCli.customers.retrieve(
|
||||
customer.processor.id,
|
||||
{
|
||||
expand: ["test_clock", "invoice_settings.default_payment_method"],
|
||||
},
|
||||
);
|
||||
return stripeCus as Stripe.Customer;
|
||||
} catch (error) {
|
||||
createNew = true;
|
||||
}
|
||||
@@ -70,9 +76,9 @@ export const createStripeCusIfNotExists = async ({
|
||||
id: stripeCustomer.id,
|
||||
type: ProcessorType.Stripe,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
return stripeCustomer;
|
||||
}
|
||||
};
|
||||
|
||||
export const createStripeCustomer = async ({
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Organization,
|
||||
ErrCode,
|
||||
BillingInterval,
|
||||
Reward,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import { getCusPaymentMethod } from "../stripeCusUtils.js";
|
||||
@@ -27,6 +28,7 @@ export const createStripeSub = async ({
|
||||
anchorToUnix,
|
||||
itemSet,
|
||||
now,
|
||||
reward,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
stripeCli: Stripe;
|
||||
@@ -37,6 +39,7 @@ export const createStripeSub = async ({
|
||||
anchorToUnix?: number;
|
||||
itemSet: ItemSet;
|
||||
now?: number;
|
||||
reward?: Reward;
|
||||
}) => {
|
||||
let paymentMethod = await getCusPaymentMethod({
|
||||
stripeCli,
|
||||
@@ -82,6 +85,8 @@ export const createStripeSub = async ({
|
||||
billing_cycle_anchor: billingCycleAnchorUnix
|
||||
? Math.floor(billingCycleAnchorUnix / 1000)
|
||||
: undefined,
|
||||
|
||||
coupon: reward ? reward.id : undefined,
|
||||
});
|
||||
|
||||
// Store
|
||||
|
||||
@@ -109,6 +109,7 @@ export const handleInvoicePaidDiscount = async ({
|
||||
`Coupon ${couponId}, stripeCus: ${stripeCus.id}: credits used up or expired. discountFinished: ${discountFinished}, expired: ${expired}`,
|
||||
);
|
||||
|
||||
if (expandedInvoice.subscription) {
|
||||
await deleteCouponFromCus({
|
||||
stripeCli,
|
||||
stripeSubId: expandedInvoice.subscription as string,
|
||||
@@ -116,6 +117,8 @@ export const handleInvoicePaidDiscount = async ({
|
||||
discountId: discount.id,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const init = async () => {
|
||||
"http://localhost:3000",
|
||||
"https://app.useautumn.com",
|
||||
"https://*.useautumn.com",
|
||||
"https://localhost:8080",
|
||||
process.env.CLIENT_URL || "",
|
||||
],
|
||||
credentials: true,
|
||||
|
||||
@@ -290,6 +290,8 @@ entitledRouter.post("", async (req: any, res: any) => {
|
||||
entity_id,
|
||||
} = req.body;
|
||||
|
||||
console.log("BODY:", req.body);
|
||||
|
||||
const { logtail: logger, db } = req;
|
||||
|
||||
if (!customer_id) {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import express, { Router } from "express";
|
||||
import {
|
||||
CreateRewardSchema,
|
||||
ErrCode,
|
||||
RewardCategory,
|
||||
RewardType,
|
||||
} from "@autumn/shared";
|
||||
import express from "express";
|
||||
import { CreateRewardSchema, ErrCode, RewardCategory } from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
@@ -37,6 +37,7 @@ export const handleOneOffFunction = async ({
|
||||
products,
|
||||
prices,
|
||||
optionsList,
|
||||
reward,
|
||||
} = attachParams;
|
||||
|
||||
const { invoiceOnly } = config;
|
||||
@@ -89,6 +90,13 @@ export const handleOneOffFunction = async ({
|
||||
customer: customer.processor.id!,
|
||||
auto_advance: false,
|
||||
currency: org.default_currency!,
|
||||
discounts: reward
|
||||
? [
|
||||
{
|
||||
coupon: reward.id,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
logger.info("2. Creating invoice items");
|
||||
|
||||
@@ -44,6 +44,7 @@ export const handlePaidProduct = async ({
|
||||
invoiceOnly,
|
||||
cusProducts,
|
||||
stripeCli,
|
||||
reward,
|
||||
} = attachParams;
|
||||
|
||||
if (attachParams.disableFreeTrial) {
|
||||
@@ -56,7 +57,6 @@ export const handlePaidProduct = async ({
|
||||
});
|
||||
|
||||
let subscriptions: Stripe.Subscription[] = [];
|
||||
let invoiceIds: string[] = [];
|
||||
|
||||
// Only merge if no free trials
|
||||
let mergeCusProduct = undefined;
|
||||
@@ -71,7 +71,8 @@ export const handlePaidProduct = async ({
|
||||
subIds: mergeCusProduct?.subscription_ids,
|
||||
});
|
||||
|
||||
for (const itemSet of itemSets) {
|
||||
for (let i = 0; i < itemSets.length; i++) {
|
||||
const itemSet = itemSets[i];
|
||||
if (itemSet.interval === BillingInterval.OneOff) {
|
||||
continue;
|
||||
}
|
||||
@@ -104,6 +105,7 @@ export const handlePaidProduct = async ({
|
||||
invoiceOnly,
|
||||
itemSet,
|
||||
anchorToUnix: billingCycleAnchorUnix,
|
||||
reward: i == 0 ? reward : undefined,
|
||||
});
|
||||
|
||||
let sub = subscription as Stripe.Subscription;
|
||||
|
||||
@@ -144,14 +144,6 @@ export const checkStripeConnections = async ({
|
||||
const logger = req.logtail;
|
||||
const env = customer.env;
|
||||
|
||||
if (!org.stripe_connected) {
|
||||
throw new RecaseError({
|
||||
message: "Please connect to Stripe to add products",
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If invoice only and no email, save email
|
||||
if (attachParams.invoiceOnly && !customer.email) {
|
||||
customer.email = `${customer.id}@invoices.useautumn.com`;
|
||||
@@ -165,13 +157,13 @@ export const checkStripeConnections = async ({
|
||||
}
|
||||
|
||||
const batchProductUpdates = [
|
||||
createStripeCusIfNotExists({
|
||||
db: req.db,
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
logger,
|
||||
}),
|
||||
// createStripeCusIfNotExists({
|
||||
// db: req.db,
|
||||
// org,
|
||||
// env,
|
||||
// customer,
|
||||
// logger,
|
||||
// }),
|
||||
];
|
||||
for (const product of products) {
|
||||
batchProductUpdates.push(
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { isOneOff } from "@/internal/products/productUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { ErrCode, CusProductStatus } from "@autumn/shared";
|
||||
import { AttachBody } from "../../../models/AttachBody.js";
|
||||
|
||||
const getProductsForAttach = async ({
|
||||
req,
|
||||
attachBody,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachBody: AttachBody;
|
||||
}) => {
|
||||
const { product_id, product_ids, version } = attachBody;
|
||||
|
||||
let products = await ProductService.listFull({
|
||||
db: req.db,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
inIds: product_ids || [product_id!],
|
||||
version,
|
||||
});
|
||||
|
||||
if (notNullish(product_ids)) {
|
||||
let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial));
|
||||
if (freeTrialProds.length > 0) {
|
||||
throw new RecaseError({
|
||||
message:
|
||||
"When providing product_ids, can't have multiple free trial products",
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
}
|
||||
|
||||
for (const prod of products) {
|
||||
let otherProd = products.find(
|
||||
(p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id,
|
||||
);
|
||||
|
||||
if (otherProd && !otherProd.is_add_on && !isOneOff(prod.prices)) {
|
||||
throw new RecaseError({
|
||||
message:
|
||||
"Can't attach multiple products from the same group that are not add-ons",
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return products;
|
||||
};
|
||||
|
||||
export const getCustomerAndProducts = async ({
|
||||
req,
|
||||
attachBody,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachBody: AttachBody;
|
||||
}) => {
|
||||
const [customer, products] = await Promise.all([
|
||||
getOrCreateCustomer({
|
||||
req,
|
||||
customerId: attachBody.customer_id,
|
||||
customerData: attachBody.customer_data,
|
||||
inStatuses: [
|
||||
CusProductStatus.Active,
|
||||
CusProductStatus.Scheduled,
|
||||
CusProductStatus.PastDue,
|
||||
],
|
||||
withEntities: true,
|
||||
entityId: attachBody.entity_id || undefined,
|
||||
entityData: attachBody.entity_data,
|
||||
}),
|
||||
getProductsForAttach({ req, attachBody }),
|
||||
]);
|
||||
|
||||
return { customer, products };
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
cusProductToPrices,
|
||||
cusProductToEnts,
|
||||
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
import {
|
||||
getFreeTrialAfterFingerprint,
|
||||
handleNewFreeTrial,
|
||||
} from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { isMainProduct } from "@/internal/products/productUtils/classifyProduct.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
Price,
|
||||
Entitlement,
|
||||
CreateFreeTrial,
|
||||
} from "@autumn/shared";
|
||||
import { AttachBody } from "../../../models/AttachBody.js";
|
||||
import { mapOptionsList } from "../../mapOptionsList.js";
|
||||
|
||||
export const getPricesAndEnts = async ({
|
||||
req,
|
||||
attachBody,
|
||||
customer,
|
||||
products,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachBody: AttachBody;
|
||||
customer: FullCustomer;
|
||||
products: FullProduct[];
|
||||
}) => {
|
||||
const { options: optionsInput, is_custom, items, free_trial } = attachBody;
|
||||
const { features, db, org, logtail: logger } = req;
|
||||
|
||||
const { curMainProduct, curSameProduct } = getExistingCusProducts({
|
||||
product: products[0],
|
||||
cusProducts: customer.customer_products,
|
||||
internalEntityId: customer.entity?.internal_id,
|
||||
});
|
||||
|
||||
// Not custom
|
||||
if (!is_custom) {
|
||||
let prices = products.flatMap((p: FullProduct) => p.prices);
|
||||
let entitlements = products.flatMap((p: FullProduct) => p.entitlements);
|
||||
|
||||
let freeTrial = null;
|
||||
let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
|
||||
if (freeTrialProduct) {
|
||||
freeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: freeTrialProduct.free_trial,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: freeTrialProduct.id,
|
||||
});
|
||||
}
|
||||
|
||||
const prodIsMain = isMainProduct({ product: products[0], prices });
|
||||
|
||||
return {
|
||||
optionsList: mapOptionsList({
|
||||
optionsInput: optionsInput || [],
|
||||
features,
|
||||
prices,
|
||||
curCusProduct: curMainProduct,
|
||||
}),
|
||||
prices,
|
||||
entitlements,
|
||||
freeTrial,
|
||||
cusProducts: customer.customer_products,
|
||||
};
|
||||
}
|
||||
|
||||
const product = products[0];
|
||||
|
||||
let curPrices: Price[] = product!.prices;
|
||||
let curEnts: Entitlement[] = product!.entitlements;
|
||||
|
||||
if (curMainProduct?.product.id === product.id) {
|
||||
curPrices = cusProductToPrices({ cusProduct: curMainProduct });
|
||||
curEnts = cusProductToEnts({ cusProduct: curMainProduct });
|
||||
}
|
||||
|
||||
let {
|
||||
prices,
|
||||
entitlements: ents,
|
||||
customPrices,
|
||||
customEnts,
|
||||
} = await handleNewProductItems({
|
||||
db,
|
||||
curPrices,
|
||||
curEnts,
|
||||
newItems: attachBody.items || [],
|
||||
features,
|
||||
product,
|
||||
logger,
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
const freeTrial = await handleNewFreeTrial({
|
||||
db,
|
||||
curFreeTrial: product!.free_trial,
|
||||
newFreeTrial: (free_trial as CreateFreeTrial) || null,
|
||||
internalProductId: product!.internal_id,
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: freeTrial,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: product.id,
|
||||
});
|
||||
|
||||
return {
|
||||
optionsList: mapOptionsList({
|
||||
optionsInput: optionsInput || [],
|
||||
features,
|
||||
prices,
|
||||
curCusProduct: curMainProduct,
|
||||
}),
|
||||
prices,
|
||||
entitlements: getEntsWithFeature({
|
||||
ents,
|
||||
features,
|
||||
}),
|
||||
freeTrial: uniqueFreeTrial,
|
||||
customPrices,
|
||||
customEnts,
|
||||
};
|
||||
};
|
||||
@@ -1,39 +1,61 @@
|
||||
import { listCusPaymentMethods } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import {
|
||||
createStripeCusIfNotExists,
|
||||
listCusPaymentMethods,
|
||||
} from "@/external/stripe/stripeCusUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { AppEnv, Customer, ErrCode, Organization } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const getStripeCusData = async ({
|
||||
stripeCli,
|
||||
stripeId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
logger,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
stripeId?: string;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
customer: Customer;
|
||||
logger: any;
|
||||
}) => {
|
||||
if (!stripeId) {
|
||||
return { stripeCus: undefined, paymentMethod: null, now: Date.now() };
|
||||
if (!org.stripe_connected) {
|
||||
throw new RecaseError({
|
||||
message: "Please connect to Stripe to add products",
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
let stripeCus = await stripeCli.customers.retrieve(stripeId, {
|
||||
expand: ["test_clock", "invoice_settings.default_payment_method"],
|
||||
});
|
||||
let stripeCus = (await createStripeCusIfNotExists({
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
logger,
|
||||
})) as Stripe.Customer;
|
||||
|
||||
let stripeCusData = stripeCus as Stripe.Customer;
|
||||
let testClock =
|
||||
stripeCusData.test_clock as Stripe.TestHelpers.TestClock | null;
|
||||
let testClock = stripeCus.test_clock as Stripe.TestHelpers.TestClock | null;
|
||||
|
||||
// let now = testClock ? testClock.frozen_time * 1000 : Date.now();
|
||||
let now = testClock ? testClock.frozen_time * 1000 : undefined;
|
||||
|
||||
let paymentMethod = stripeCusData.invoice_settings
|
||||
let paymentMethod = stripeCus.invoice_settings
|
||||
?.default_payment_method as Stripe.PaymentMethod | null;
|
||||
|
||||
if (!paymentMethod) {
|
||||
let paymentMethods = await listCusPaymentMethods({
|
||||
stripeCli,
|
||||
stripeId,
|
||||
stripeId: stripeCus.id,
|
||||
});
|
||||
|
||||
paymentMethod = paymentMethods.length ? paymentMethods[0] : null;
|
||||
}
|
||||
|
||||
return { stripeCus: stripeCusData, paymentMethod, now };
|
||||
return { stripeCus, paymentMethod, now };
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export const getAttachParams = async ({
|
||||
customPrices,
|
||||
customEnts,
|
||||
stripeVars,
|
||||
reward,
|
||||
} = await processAttachBody({
|
||||
req,
|
||||
attachBody,
|
||||
@@ -52,7 +53,7 @@ export const getAttachParams = async ({
|
||||
entitlements,
|
||||
freeTrial,
|
||||
replaceables: [],
|
||||
|
||||
reward,
|
||||
// From req
|
||||
req,
|
||||
org: req.org,
|
||||
|
||||
@@ -1,219 +1,49 @@
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AttachBody } from "../../models/AttachBody.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
CreateFreeTrial,
|
||||
CusProductStatus,
|
||||
Entitlement,
|
||||
ErrCode,
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
Price,
|
||||
} from "@autumn/shared";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { getOrCreateCustomer } from "../../../cusUtils/getOrCreateCustomer.js";
|
||||
import { getExistingCusProducts } from "../../../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
import { mapOptionsList } from "../mapOptionsList.js";
|
||||
import {
|
||||
getFreeTrialAfterFingerprint,
|
||||
handleNewFreeTrial,
|
||||
} from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import {
|
||||
cusProductToEnts,
|
||||
cusProductToPrices,
|
||||
} from "../../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
import { isMainProduct } from "@/internal/products/productUtils/classifyProduct.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js";
|
||||
import { isOneOff } from "@/internal/products/productUtils.js";
|
||||
import { getPricesAndEnts } from "./attachParamsUtils/getPricesAndEnts.js";
|
||||
import { getCustomerAndProducts } from "./attachParamsUtils/getCusAndProducts.js";
|
||||
import { RewardService } from "@/internal/rewards/RewardService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const getProductsForAttach = async ({
|
||||
export const getReward = async ({
|
||||
req,
|
||||
attachBody,
|
||||
stripeCli,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachBody: AttachBody;
|
||||
stripeCli: Stripe;
|
||||
}) => {
|
||||
const { product_id, product_ids, version } = attachBody;
|
||||
const { reward: idOrCode } = attachBody;
|
||||
if (!idOrCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let products = await ProductService.listFull({
|
||||
// 1. Get reward by id or promo code
|
||||
const reward = await RewardService.getByIdOrCode({
|
||||
db: req.db,
|
||||
orgId: req.orgId,
|
||||
idOrCode,
|
||||
orgId: req.org.id,
|
||||
env: req.env,
|
||||
inIds: product_ids || [product_id!],
|
||||
version,
|
||||
});
|
||||
|
||||
if (notNullish(product_ids)) {
|
||||
let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial));
|
||||
if (freeTrialProds.length > 0) {
|
||||
if (!reward) {
|
||||
throw new RecaseError({
|
||||
message:
|
||||
"When providing product_ids, can't have multiple free trial products",
|
||||
code: ErrCode.InvalidRequest,
|
||||
message: `Reward ${idOrCode} not found`,
|
||||
code: ErrCode.RewardNotFound,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
for (const prod of products) {
|
||||
let otherProd = products.find(
|
||||
(p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id,
|
||||
);
|
||||
|
||||
if (otherProd && !otherProd.is_add_on && !isOneOff(prod.prices)) {
|
||||
throw new RecaseError({
|
||||
message:
|
||||
"Can't attach multiple products from the same group that are not add-ons",
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return products;
|
||||
};
|
||||
|
||||
const getCustomerAndProducts = async ({
|
||||
req,
|
||||
attachBody,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachBody: AttachBody;
|
||||
}) => {
|
||||
const [customer, products] = await Promise.all([
|
||||
getOrCreateCustomer({
|
||||
req,
|
||||
customerId: attachBody.customer_id,
|
||||
customerData: attachBody.customer_data,
|
||||
inStatuses: [
|
||||
CusProductStatus.Active,
|
||||
CusProductStatus.Scheduled,
|
||||
CusProductStatus.PastDue,
|
||||
],
|
||||
withEntities: true,
|
||||
entityId: attachBody.entity_id || undefined,
|
||||
entityData: attachBody.entity_data,
|
||||
}),
|
||||
getProductsForAttach({ req, attachBody }),
|
||||
]);
|
||||
|
||||
return { customer, products };
|
||||
};
|
||||
|
||||
const getPricesAndEnts = async ({
|
||||
req,
|
||||
attachBody,
|
||||
customer,
|
||||
products,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachBody: AttachBody;
|
||||
customer: FullCustomer;
|
||||
products: FullProduct[];
|
||||
}) => {
|
||||
const { options: optionsInput, is_custom, items, free_trial } = attachBody;
|
||||
const { features, db, org, logtail: logger } = req;
|
||||
|
||||
const { curMainProduct, curSameProduct } = getExistingCusProducts({
|
||||
product: products[0],
|
||||
cusProducts: customer.customer_products,
|
||||
internalEntityId: customer.entity?.internal_id,
|
||||
});
|
||||
|
||||
// Not custom
|
||||
if (!is_custom) {
|
||||
let prices = products.flatMap((p: FullProduct) => p.prices);
|
||||
let entitlements = products.flatMap((p: FullProduct) => p.entitlements);
|
||||
|
||||
let freeTrial = null;
|
||||
let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
|
||||
if (freeTrialProduct) {
|
||||
freeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: freeTrialProduct.free_trial,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: freeTrialProduct.id,
|
||||
});
|
||||
}
|
||||
|
||||
const prodIsMain = isMainProduct({ product: products[0], prices });
|
||||
const stripeCoupon = await stripeCli.coupons.retrieve(reward.id);
|
||||
|
||||
return {
|
||||
optionsList: mapOptionsList({
|
||||
optionsInput: optionsInput || [],
|
||||
features,
|
||||
prices,
|
||||
curCusProduct: curMainProduct,
|
||||
}),
|
||||
prices,
|
||||
entitlements,
|
||||
freeTrial,
|
||||
cusProducts: customer.customer_products,
|
||||
};
|
||||
}
|
||||
|
||||
const product = products[0];
|
||||
|
||||
let curPrices: Price[] = product!.prices;
|
||||
let curEnts: Entitlement[] = product!.entitlements;
|
||||
|
||||
if (curMainProduct?.product.id === product.id) {
|
||||
curPrices = cusProductToPrices({ cusProduct: curMainProduct });
|
||||
curEnts = cusProductToEnts({ cusProduct: curMainProduct });
|
||||
}
|
||||
|
||||
let {
|
||||
prices,
|
||||
entitlements: ents,
|
||||
customPrices,
|
||||
customEnts,
|
||||
} = await handleNewProductItems({
|
||||
db,
|
||||
curPrices,
|
||||
curEnts,
|
||||
newItems: attachBody.items || [],
|
||||
features,
|
||||
product,
|
||||
logger,
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
const freeTrial = await handleNewFreeTrial({
|
||||
db,
|
||||
curFreeTrial: product!.free_trial,
|
||||
newFreeTrial: (free_trial as CreateFreeTrial) || null,
|
||||
internalProductId: product!.internal_id,
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: freeTrial,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: product.id,
|
||||
});
|
||||
|
||||
return {
|
||||
optionsList: mapOptionsList({
|
||||
optionsInput: optionsInput || [],
|
||||
features,
|
||||
prices,
|
||||
curCusProduct: curMainProduct,
|
||||
}),
|
||||
prices,
|
||||
entitlements: getEntsWithFeature({
|
||||
ents,
|
||||
features,
|
||||
}),
|
||||
freeTrial: uniqueFreeTrial,
|
||||
customPrices,
|
||||
customEnts,
|
||||
reward,
|
||||
stripeCoupon,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -233,10 +63,22 @@ export const processAttachBody = async ({
|
||||
});
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
let stripeCusData = await getStripeCusData({
|
||||
const [stripeCusData, rewardData] = await Promise.all([
|
||||
getStripeCusData({
|
||||
stripeCli,
|
||||
stripeId: customer.processor?.id,
|
||||
});
|
||||
db: req.db,
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
logger: req.logtail,
|
||||
}),
|
||||
getReward({
|
||||
req,
|
||||
attachBody,
|
||||
stripeCli,
|
||||
}),
|
||||
]);
|
||||
|
||||
const { stripeCus, paymentMethod, now } = stripeCusData;
|
||||
|
||||
@@ -257,6 +99,7 @@ export const processAttachBody = async ({
|
||||
return {
|
||||
customer,
|
||||
products,
|
||||
reward: rewardData?.reward,
|
||||
optionsList,
|
||||
prices,
|
||||
entitlements,
|
||||
|
||||
@@ -46,6 +46,7 @@ export const AttachBodySchema = z
|
||||
metadata: z.any().optional(),
|
||||
billing_cycle_anchor: z.number().optional(),
|
||||
checkout_session_params: z.any().optional(),
|
||||
reward: z.string().optional(),
|
||||
})
|
||||
.refine((data) => !(data.product_id && data.product_ids), {
|
||||
message: "Either product_id or product_ids should be provided, not both",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
APIVersion,
|
||||
FullCustomer,
|
||||
AttachReplaceable,
|
||||
Reward,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
@@ -25,6 +26,7 @@ export type AttachParams = {
|
||||
stripeCus?: Stripe.Customer;
|
||||
now?: number;
|
||||
paymentMethod: Stripe.PaymentMethod | null | undefined;
|
||||
reward?: Reward;
|
||||
|
||||
org: Organization;
|
||||
// customer: Customer;
|
||||
|
||||
@@ -10,10 +10,10 @@ import { devRouter } from "./dev/devRouter.js";
|
||||
import { cusRouter } from "./customers/internalCusRouter.js";
|
||||
import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
|
||||
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
|
||||
import { Autumn } from "autumn-js";
|
||||
import { autumnHandler } from "autumn-js/express";
|
||||
import { withAdminAuth } from "./admin/withAdminAuth.js";
|
||||
import { adminRouter } from "./admin/adminRouter.js";
|
||||
import { autumnHandler } from "autumn-js/express";
|
||||
import { Autumn } from "autumn-js";
|
||||
|
||||
const mainRouter: Router = Router();
|
||||
|
||||
@@ -57,7 +57,10 @@ mainRouter.use(
|
||||
autumn: (req: any) => {
|
||||
let client = new Autumn({
|
||||
url: "http://localhost:8080/v1",
|
||||
headers: req.headers,
|
||||
headers: {
|
||||
cookie: req.headers.cookie,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return client as any;
|
||||
},
|
||||
|
||||
@@ -134,22 +134,22 @@ export const migrateCustomers = async ({
|
||||
|
||||
// Get number of errors
|
||||
let migrationDetails: any = {};
|
||||
try {
|
||||
let errors = await MigrationService.getErrors({
|
||||
db,
|
||||
migrationJobId: migrationJob.id,
|
||||
});
|
||||
// try {
|
||||
// let errors = await MigrationService.getErrors({
|
||||
// db,
|
||||
// migrationJobId: migrationJob.id,
|
||||
// });
|
||||
|
||||
migrationDetails.num_errors = errors!.length;
|
||||
migrationDetails.failed_customers = errors!.map(
|
||||
(e: any) => `${e.customer.id} - ${e.customer.name}`,
|
||||
);
|
||||
} catch (error) {
|
||||
migrationDetails.failed_to_get_errors = true;
|
||||
migrationDetails.error = error;
|
||||
logger.error("Failed to get migration errors");
|
||||
logger.error(error);
|
||||
}
|
||||
// migrationDetails.num_errors = errors!.length;
|
||||
// migrationDetails.failed_customers = errors!.map(
|
||||
// (e: any) => `${e.customer.id} - ${e.customer.name}`,
|
||||
// );
|
||||
// } catch (error) {
|
||||
// migrationDetails.failed_to_get_errors = true;
|
||||
// migrationDetails.error = error;
|
||||
// logger.error("Failed to get migration errors");
|
||||
// logger.error(error);
|
||||
// }
|
||||
|
||||
let curMigrationJob = await MigrationService.getJob({
|
||||
db,
|
||||
|
||||
@@ -197,14 +197,6 @@ orgRouter.delete("/stripe", async (req: any, res) => {
|
||||
},
|
||||
});
|
||||
|
||||
const clerkCli = createClerkCli();
|
||||
await clerkCli.organizations.updateOrganization(req.orgId, {
|
||||
publicMetadata: {
|
||||
stripe_connected: false,
|
||||
default_currency: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
message: "Stripe disconnected",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AppEnv, ErrCode, Reward, rewards } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { and, desc, eq, or } from "drizzle-orm";
|
||||
import { and, arrayContains, desc, eq, inArray, or, sql } from "drizzle-orm";
|
||||
|
||||
export class RewardService {
|
||||
static async get({
|
||||
@@ -33,6 +33,38 @@ export class RewardService {
|
||||
return result as Reward;
|
||||
}
|
||||
|
||||
static async getByIdOrCode({
|
||||
db,
|
||||
idOrCode,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
idOrCode: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
let reward = await db.query.rewards.findFirst({
|
||||
where: and(
|
||||
eq(rewards.org_id, orgId),
|
||||
eq(rewards.env, env),
|
||||
or(
|
||||
eq(rewards.id, idOrCode),
|
||||
sql`EXISTS (
|
||||
SELECT 1 FROM unnest("promo_codes") AS elem
|
||||
WHERE elem->>'code' = ${idOrCode}
|
||||
)`,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
if (!reward) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return reward as Reward;
|
||||
}
|
||||
|
||||
static async insert({
|
||||
db,
|
||||
data,
|
||||
|
||||
179
server/tests/advanced/coupons/coupon3.ts
Normal file
179
server/tests/advanced/coupons/coupon3.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
APIVersion,
|
||||
AppEnv,
|
||||
CouponDurationType,
|
||||
CreateReward,
|
||||
Organization,
|
||||
RewardType,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import {
|
||||
constructArrearItem,
|
||||
constructFeatureItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import {
|
||||
addPrefixToProducts,
|
||||
getBasePrice,
|
||||
} from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
import { createProducts, createReward } from "tests/utils/productUtils.js";
|
||||
import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
});
|
||||
|
||||
const oneOff = constructProduct({
|
||||
type: "one_off",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 500,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Create reward input
|
||||
const rewardId = "attach_coupon";
|
||||
const promoCode = "attach_coupon_code";
|
||||
const reward: CreateReward = {
|
||||
id: rewardId,
|
||||
name: "attach_coupon",
|
||||
promo_codes: [{ code: promoCode }],
|
||||
type: RewardType.FixedDiscount,
|
||||
discount_config: {
|
||||
discount_value: 5,
|
||||
duration_type: CouponDurationType.Forever,
|
||||
duration_value: 1,
|
||||
should_rollover: true,
|
||||
apply_to_all: true,
|
||||
price_ids: [],
|
||||
},
|
||||
};
|
||||
|
||||
const testCase = "coupon3";
|
||||
describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => {
|
||||
let customerId = testCase;
|
||||
let stripeCli: Stripe;
|
||||
let testClockId: string;
|
||||
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let db: DrizzleCli;
|
||||
|
||||
let couponAmount = reward.discount_config!.discount_value;
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
db = this.db;
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
customerId,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
autumn: this.autumnJs,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro, oneOff],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
orgId: this.org.id,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
autumn,
|
||||
products: [pro, oneOff],
|
||||
});
|
||||
|
||||
await createReward({
|
||||
orgId: org.id,
|
||||
env,
|
||||
db,
|
||||
autumn,
|
||||
reward,
|
||||
productId: pro.id,
|
||||
});
|
||||
});
|
||||
|
||||
// CYCLE 0
|
||||
it("should attach pro with reward ID", async () => {
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
reward: rewardId,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectAttachCorrect({
|
||||
customer,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
const invoice = customer.invoices![0];
|
||||
let basePrice = getBasePrice({ product: pro });
|
||||
expect(invoice.total).to.equal(basePrice - couponAmount);
|
||||
});
|
||||
|
||||
it("should attach one off with reward ID", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: oneOff.id,
|
||||
reward: rewardId,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectAttachCorrect({
|
||||
customer,
|
||||
product: oneOff,
|
||||
});
|
||||
|
||||
const invoice = customer.invoices![0];
|
||||
let basePrice = getBasePrice({ product: oneOff });
|
||||
expect(invoice.total).to.equal(basePrice - couponAmount);
|
||||
expect(invoice.product_ids).to.include(oneOff.id);
|
||||
});
|
||||
|
||||
it("should attach one off with promo code", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: oneOff.id,
|
||||
reward: promoCode,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectAttachCorrect({
|
||||
customer,
|
||||
product: oneOff,
|
||||
});
|
||||
|
||||
expect(customer.invoices!.length).to.equal(3);
|
||||
let basePrice = getBasePrice({ product: oneOff });
|
||||
for (let i = 0; i < 2; i++) {
|
||||
let invoice = customer.invoices![i];
|
||||
expect(invoice.total).to.equal(basePrice - couponAmount);
|
||||
expect(invoice.product_ids).to.include(oneOff.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
Organization,
|
||||
ProductItemInterval,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { AppEnv, Organization } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
@@ -16,13 +10,9 @@ import { addPrefixToProducts, runAttachTest } from "../utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { replaceItems } from "../utils.js";
|
||||
|
||||
import { defaultApiVersion } from "tests/constants.js";
|
||||
import { runMigrationTest } from "./runMigrationTest.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addDays } from "date-fns";
|
||||
import { expect } from "chai";
|
||||
|
||||
let wordsItem = constructArrearItem({
|
||||
|
||||
@@ -11,7 +11,7 @@ export const DiscountConfigSchema = z.object({
|
||||
duration_value: z.number(),
|
||||
should_rollover: z.boolean().optional(),
|
||||
apply_to_all: z.boolean().optional(),
|
||||
price_ids: z.array(z.string()),
|
||||
price_ids: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const RewardSchema = z.object({
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"zod": "^3.25.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.3",
|
||||
"nodemon": "^3.1.7",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
"types": ["node"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user