fix type errors and tests
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user