fix type errors and tests

This commit is contained in:
John Yeo
2026-03-13 11:48:31 +00:00
parent 624d5e78d8
commit 92456de04f
16 changed files with 934 additions and 1140 deletions

View File

@@ -36,7 +36,7 @@ jobs:
- name: Run TypeScript type check
run: |
cd server && bun ts
bun ts
build-and-push:
name: Build and Push Docker Image

View File

@@ -127,7 +127,13 @@ function getTierRangeText({
}
const previousTier = tiers[index - 1];
if (!previousTier || typeof previousTier.to !== "number") return "";
if (
!previousTier ||
typeof previousTier.to !== "number" ||
typeof tier.to !== "number"
) {
return "";
}
return `for the next ${tier.to - previousTier.to}`;
}
@@ -232,8 +238,6 @@ export function PlanItemTierDetails({
return (
<Accordion
type="single"
collapsible
className={cn(
"w-auto max-w-full",
align === "right" && "items-end text-right",

View File

@@ -1,6 +1,6 @@
import {
type ApiPlanItemV1,
type BillingPreviewChange,
type GetCheckoutResponse,
} from "@autumn/shared";
import { CheckIcon, WalletIcon } from "@phosphor-icons/react";
import { AnimatePresence, motion } from "motion/react";
@@ -43,7 +43,7 @@ function getFeatureName(planItem: ApiPlanItemV1): string {
}
interface PlanSelectionCardProps {
change: BillingPreviewChange;
change: GetCheckoutResponse["preview"]["incoming"][number];
}
export function PlanSelectionCard({ change }: PlanSelectionCardProps) {

View File

@@ -1,8 +1,8 @@
import {
type BillingPreviewChange,
type BillingResponse,
CheckoutAction,
type ConfirmCheckoutResponse,
type GetCheckoutResponse,
CheckoutStatus,
} from "@autumn/shared";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -17,12 +17,14 @@ import { buildHeaderDescription } from "@/utils/buildHeaderDescription";
const SUCCESS_REDIRECT_DELAY_MS = 2000;
type CheckoutPreviewChange = GetCheckoutResponse["preview"]["incoming"][number];
function haveMatchingQuantities({
incoming,
outgoing,
}: {
incoming: BillingPreviewChange;
outgoing: BillingPreviewChange;
incoming: CheckoutPreviewChange;
outgoing: CheckoutPreviewChange;
}) {
if (incoming.feature_quantities.length !== outgoing.feature_quantities.length) {
return false;
@@ -43,7 +45,7 @@ function haveMatchingQuantities({
}
function buildFeatureQuantities(
incoming: BillingPreviewChange[],
incoming: CheckoutPreviewChange[],
quantities: Record<string, number>,
): { feature_id: string; quantity: number }[] {
const featureQuantities: { feature_id: string; quantity: number }[] = [];
@@ -123,18 +125,18 @@ export function useCheckoutState({
const isUpdateQuantityIntent =
preview?.object === "update_subscription_preview" &&
preview.intent === "update_quantity";
const matchingOutgoingChange = incoming?.[0]
? outgoing?.find((change) => change.plan_id === incoming[0].plan_id)
const incomingChange = incoming?.[0];
const matchingOutgoingChange = incomingChange
? outgoing?.find((change) => change.plan_id === incomingChange.plan_id)
: undefined;
const isUnchangedQuantityUpdate =
isUpdateQuantityIntent &&
Boolean(incoming?.[0]) &&
Boolean(matchingOutgoingChange) &&
haveMatchingQuantities({
incoming: incoming[0],
outgoing: matchingOutgoingChange,
});
const incomingPlan = incoming?.[0]?.plan;
isUpdateQuantityIntent && incomingChange && matchingOutgoingChange
? haveMatchingQuantities({
incoming: incomingChange,
outgoing: matchingOutgoingChange,
})
: false;
const incomingPlan = incomingChange?.plan;
const freeTrial = incomingPlan?.free_trial;
const hasActiveTrial = !!freeTrial;

View File

@@ -1,15 +1,16 @@
import type {
ApiFreeTrialV2,
AttachPreviewResponse,
BillingPreviewChange,
BillingPreviewResponse,
CheckoutEntity,
PreviewUpdateSubscriptionResponse,
GetCheckoutResponse,
} from "@autumn/shared";
import { format } from "date-fns";
import { formatAmount } from "./formatUtils";
import { getCheckoutPreviewIntent } from "./getCheckoutPreviewIntent";
type CheckoutPreview = GetCheckoutResponse["preview"];
type CheckoutPreviewChange = CheckoutPreview["incoming"][number];
/**
* Builds a phrase describing applied discounts.
* Examples: "Discount code 20OFF applied for 20% off.", "Discount codes 20OFF (20% off) and SAVE10 ($10 off) applied."
@@ -130,9 +131,9 @@ export function buildHeaderDescription({
freeTrial,
hasActiveTrial,
}: {
preview?: AttachPreviewResponse | PreviewUpdateSubscriptionResponse;
incoming?: BillingPreviewChange[];
outgoing?: BillingPreviewChange[];
preview?: CheckoutPreview;
incoming?: CheckoutPreviewChange[];
outgoing?: CheckoutPreviewChange[];
entity?: CheckoutEntity;
freeTrial?: ApiFreeTrialV2 | null;
hasActiveTrial?: boolean;

View File

@@ -8,7 +8,7 @@ const res = await autumn.customers.getOrCreate({
customerId: "john",
});
await autumn.entities.create({
const entity = await autumn.entities.create({
customerId: "john",
entityId: "name",
featureId: "user",
@@ -23,4 +23,4 @@ await autumn.entities.create({
},
});
console.log(JSON.stringify(res, null, 2));
// console.log(JSON.stringify(entity.billingControls, null, 2));

View File

@@ -51,6 +51,10 @@ export function augmentCheckoutParams({
| AttachParamsV1["feature_quantities"]
| UpdateSubscriptionV1Params["feature_quantities"];
}) => {
if (!body.feature_quantities) {
return originalFeatureQuantities;
}
return body.feature_quantities.map((featureQuantity) => {
const originalFeatureQuantity = originalFeatureQuantities?.find(
(original) => original.feature_id === featureQuantity.feature_id,

View File

@@ -6,29 +6,12 @@ export const temp: TestGroup = {
tier: "domain",
paths: [
// Cancel immediately with default tests
"integration/billing/stripe-webhooks/subscription-deleted/subscription-deleted-invoice.test.ts",
"integration/billing/attach/scheduled-switch/scheduled-switch-consumable.test.ts",
"integration/billing/legacy/attach/invoice/legacy-attach-invoice-mode.test.ts",
"integration/billing/legacy/attach/upgrade/legacy-upgrade-merged.test.ts",
"integration/billing/legacy/attach/upgrade/legacy-upgrade-usage.test.ts",
"integration/billing/migrations/migrate-trials.test.ts",
"integration/billing/update-subscription/custom-plan/update-paid-basic.test.ts",
"integration/billing/legacy/attach/new/legacy-new-merged.test.ts",
"integration/billing/legacy/attach/update-quantity/legacy-update-quantity.test.ts",
"integration/billing/migrations/migrate-free.test.ts",
"integration/billing/migrations/migrate-states.test.ts",
"integration/billing/update-subscription/cancel/uncancel/uncancel-combined.test.ts",
"integration/billing/update-subscription/custom-plan/update-paid-prepaid.test.ts",
"integration/billing/attach/invoice/attach-invoice-draft-deferred.test.ts",
"integration/billing/legacy/attach/invoice/payment-failure/legacy-attach-payment-failed.test.ts",
"integration/billing/setup-payment/setup-payment-with-customize.test.ts",
"integration/billing/update-subscription/custom-plan/update-free-to-paid.test.ts",
"integration/billing/update-subscription/free-trial/update-paid-trials.test.ts",
"integration/billing/legacy/attach/checkout/legacy-checkout-basic.test.ts",
"integration/billing/migrations/migrate-paid.test.ts",
"integration/billing/multi-attach/checkout/multi-attach-checkout-basic.test.ts",
"integration/billing/update-subscription/cancel/uncancel/uncancel-basic.test.ts",
"integration/billing/update-subscription/custom-plan/update-paid-features.test.ts",
"integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts",
"integration/billing/attach/immediate-switch/paid-features/immediate-switch-prepaid-no-options-basic.test.ts",
"integration/billing/attach/immediate-switch/paid-features/immediate-switch-prepaid-no-options-advanced.test.ts",
"integration/billing/attach/free-trial/override/trial-override-basic.test.ts",
"integration/billing/attach/free-trial/override/trial-override-merge.test.ts",
"integration/billing/attach/free-trial/trial-upgrade.test.ts",
"integration/billing/attach/free-trial/trial-entity-upgrade.test.ts",
],
};

View File

@@ -309,6 +309,7 @@ test.concurrent(`${chalk.yellowBright("trial-override-basic 3: override bypasses
customer,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(14),
toleranceMs: ms.hours(2),
});
// Verify feature reset aligns with fresh trial (14 days from now)
@@ -319,6 +320,7 @@ test.concurrent(`${chalk.yellowBright("trial-override-basic 3: override bypasses
balance: 500,
usage: 0,
resetsAt: advancedTo + ms.days(14), // Fresh trial, reset at trial end
toleranceMs: ms.hours(2),
});
// Verify $0 invoice during trial (Stripe creates invoice for trial subscriptions)

View File

@@ -330,12 +330,14 @@ test.concurrent(`${chalk.yellowBright("trial-override-merge 3: add-on with free_
customer,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(14),
toleranceMs: ms.hours(1) + ms.minutes(10),
});
await expectProductTrialing({
customer,
productId: addon.id,
trialEndsAt: advancedTo + ms.days(14),
toleranceMs: ms.hours(1) + ms.minutes(10),
});
// Verify $0 invoice during trial (Stripe creates invoice for trial subscriptions)

View File

@@ -454,6 +454,7 @@ test.concurrent(`${chalk.yellowBright("trial-upgrade 4: mid-trial upgrade to pre
customer,
productId: premiumTrial.id,
trialEndsAt: advancedTo + ms.days(14),
toleranceMs: ms.hours(1) + ms.minutes(10),
});
// Verify feature balance is premium's balance with resetsAt aligned to new trial end
@@ -464,6 +465,7 @@ test.concurrent(`${chalk.yellowBright("trial-upgrade 4: mid-trial upgrade to pre
balance: 1000,
usage: 0,
resetsAt: advancedTo + ms.days(14),
toleranceMs: ms.hours(1) + ms.minutes(10),
});
// Verify NO paid invoice generated - both are $0 trial invoices

View File

@@ -0,0 +1,479 @@
/**
* Immediate Switch Prepaid No-Options Tests (Attach V2)
*
* Advanced prepaid no-options scenarios, including price changes, usage,
* multi-feature behavior, and explicit overrides.
*/
import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options advanced 1: price increases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-price-inc";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 15,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(40);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options advanced 2: price decreases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-price-dec";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 15,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(20);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options advanced 3: with usage")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-with-usage";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
await new Promise((r) => setTimeout(r, 4000));
await autumnV1.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 50,
});
await new Promise((r) => setTimeout(r, 2000));
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 150,
usage: 50,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(30);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options advanced 4: multiple prepaid partial options")}`, async () => {
const customerId = "imm-switch-prepaid-partial-opts";
const proMessagesPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const proWordsPrepaid = items.prepaid({
featureId: TestFeature.Words,
includedUsage: 0,
billingUnits: 100,
price: 5,
});
const pro = products.pro({
id: "pro",
items: [proMessagesPrepaid, proWordsPrepaid],
});
const premiumMessagesPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premiumWordsPrepaid = items.prepaid({
featureId: TestFeature.Words,
includedUsage: 0,
billingUnits: 100,
price: 5,
});
const premium = products.premium({
id: "premium",
items: [premiumMessagesPrepaid, premiumWordsPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 200 },
{ feature_id: TestFeature.Words, quantity: 500 },
],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
});
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Words,
balance: 500,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 300 }],
});
expect(preview.total).toBe(40);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 300 }],
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 300,
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
balance: 500,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options advanced 5: all config changes")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-all-change";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 50,
billingUnits: 50,
price: 15,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(55);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options advanced 6: options explicitly set to 0")}`, async () => {
const customerId = "imm-switch-prepaid-opts-zero";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 0 }],
});
expect(preview.total).toBe(10);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
options: [{ feature_id: TestFeature.Messages, quantity: 0 }],
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 0,
usage: 0,
});
});

View File

@@ -0,0 +1,384 @@
/**
* Immediate Switch Prepaid No-Options Tests (Attach V2)
*
* Basic carry-over scenarios for upgrades involving prepaid features where
* options are not passed on upgrade.
*/
import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options basic 1: quantity carries over")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-carry";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(30);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
await expectCustomerInvoiceCorrect({
customer,
count: 2,
latestTotal: 30,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options basic 2: billing units 100 to 50")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-units-100-50";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 50,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(50);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options basic 3: billing units 50 to 100")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-units-50-100";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 50,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(10);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options basic 4: included usage increases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-incl-inc";
const proPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 100,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(20);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});
test.concurrent(`${chalk.yellowBright("immediate-switch-prepaid-no-options basic 5: included usage decreases")}`, async () => {
const customerId = "imm-switch-prepaid-no-opts-incl-dec";
const proPrepaid = items.prepaidMessages({
includedUsage: 100,
billingUnits: 100,
price: 10,
});
const pro = products.pro({
id: "pro",
items: [proPrepaid],
});
const premiumPrepaid = items.prepaidMessages({
includedUsage: 0,
billingUnits: 100,
price: 10,
});
const premium = products.premium({
id: "premium",
items: [premiumPrepaid],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro, premium] }),
],
actions: [
s.billing.attach({
productId: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 200 }],
}),
],
});
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerBefore,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
const preview = await autumnV1.billing.previewAttach({
customer_id: customerId,
product_id: premium.id,
});
expect(preview.total).toBe(40);
await autumnV1.billing.attach({
customer_id: customerId,
product_id: premium.id,
redirect_mode: "if_required",
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [premium.id],
notPresent: [pro.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 200,
usage: 0,
});
});

View File

@@ -26,15 +26,23 @@ export const confirmAutumnCheckout = async ({
checkoutId,
customerId,
productId,
featureQuantities,
}: {
checkoutId: string;
customerId: string;
productId: string;
featureQuantities?: Array<{ feature_id: string; quantity: number }>;
}): Promise<ConfirmCheckoutResponse> => {
const response = await fetch(
`${CHECKOUT_BASE_URL}/checkouts/${checkoutId}/confirm`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
featureQuantities ? { feature_quantities: featureQuantities } : {},
),
signal: AbortSignal.timeout(CHECKOUT_TIMEOUT_MS),
},
);
@@ -56,6 +64,7 @@ export const confirmAutumnCheckoutAndGetCustomer = async ({
checkoutId,
customerId,
productId,
featureQuantities,
}: {
autumnV1: {
customers: {
@@ -65,11 +74,13 @@ export const confirmAutumnCheckoutAndGetCustomer = async ({
checkoutId: string;
customerId: string;
productId: string;
featureQuantities?: Array<{ feature_id: string; quantity: number }>;
}) => {
const confirmData = await confirmAutumnCheckout({
checkoutId,
customerId,
productId,
featureQuantities,
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);

View File

@@ -2,12 +2,14 @@ import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels"
import { z } from "zod/v4";
export const ConfirmCheckoutParamsSchema = z.object({
feature_quantities: z.array(
FeatureOptionsSchema.pick({
feature_id: true,
quantity: true,
}),
),
feature_quantities: z
.array(
FeatureOptionsSchema.pick({
feature_id: true,
quantity: true,
}),
)
.optional(),
});
export type ConfirmCheckoutParams = z.infer<typeof ConfirmCheckoutParamsSchema>;