fix: attach no billing changes
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -152,4 +152,6 @@ server/.turbo
|
||||
|
||||
|
||||
./AGENTS.md
|
||||
./CLAUDE.md
|
||||
./CLAUDE.md
|
||||
|
||||
notes.txt
|
||||
2
ai
2
ai
Submodule ai updated: 761b842553...ade6ce69f0
@@ -63,7 +63,7 @@
|
||||
"@hatchet-dev/typescript-sdk": "1.14.0",
|
||||
"@hono-rate-limiter/redis": "^0.1.4",
|
||||
"@hono/node-server": "^1.19.5",
|
||||
"@hono/otel": "^1.1.1",
|
||||
"@hono/otel": "1.1.1",
|
||||
"@hono/zod-validator": "^0.7.3",
|
||||
"@hyperbrowser/sdk": "^0.54.0",
|
||||
"@infisical/sdk": "^4.0.6",
|
||||
|
||||
@@ -122,11 +122,15 @@ export const setupAttachBillingContext = async ({
|
||||
isTransitionFromFree &&
|
||||
hasPaidRecurringSubscription);
|
||||
|
||||
const skipBillingFetching =
|
||||
orgDisableStripeWrites({ ctx }) || params.no_billing_changes === true;
|
||||
// no_billing_changes blocks WRITES but should still allow reading the
|
||||
// existing Stripe sub when one is linked — needed so the new cusProduct
|
||||
// inherits subscription_ids and the paid-product guard doesn't misfire.
|
||||
const skipBillingFetching = orgDisableStripeWrites({ ctx });
|
||||
|
||||
const skipBillingChangesBase =
|
||||
skipBillingFetching || params.processor_subscription_id !== undefined;
|
||||
skipBillingFetching ||
|
||||
params.no_billing_changes === true ||
|
||||
params.processor_subscription_id !== undefined;
|
||||
|
||||
const {
|
||||
stripeSubscription,
|
||||
@@ -144,7 +148,8 @@ export const setupAttachBillingContext = async ({
|
||||
params,
|
||||
newBillingSubscription: shouldForceNewSubscription,
|
||||
skipBillingFetching,
|
||||
createStripeCustomerIfMissing: !preview,
|
||||
createStripeCustomerIfMissing:
|
||||
!preview && params.no_billing_changes !== true,
|
||||
});
|
||||
|
||||
const featureQuantities = setupFeatureQuantitiesContext({
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { ApiCustomerV3, AttachParamsV0Input } from "@autumn/shared";
|
||||
import type {
|
||||
ApiCustomerV3,
|
||||
AttachParamsV0Input,
|
||||
AttachParamsV1Input,
|
||||
} from "@autumn/shared";
|
||||
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
|
||||
import { expectNoStripeSubscription } from "@tests/integration/billing/utils/expectNoStripeSubscription";
|
||||
@@ -73,3 +77,70 @@ test.concurrent(`${chalk.yellowBright("no_billing_changes: attach with no_billin
|
||||
env: ctx.env,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression: attaching with no_billing_changes:true on a customer whose
|
||||
* current paid product is linked to a Stripe subscription used to fail with
|
||||
* "paid but no stripe subscription is linked to it", because skipBillingFetching
|
||||
* short-circuited setupStripeBillingContext and the guard read the (undefined)
|
||||
* runtime-fetched stripeSubscription. Fix decouples no_billing_changes from
|
||||
* skipBillingFetching: writes are still suppressed, but the sub is read so its
|
||||
* id carries over to the new cusProduct.
|
||||
*/
|
||||
test.concurrent(`${chalk.yellowBright("no_billing_changes: carries subscription_ids forward when current paid product is linked")}`, async () => {
|
||||
const customerId = "no-billing-changes-paid-current";
|
||||
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
const pro = products.pro({
|
||||
id: "pro-nbc-paid-current",
|
||||
items: [messagesItem],
|
||||
});
|
||||
const premium = products.premium({
|
||||
id: "premium-nbc-paid-current",
|
||||
items: [items.monthlyMessages({ includedUsage: 500 })],
|
||||
});
|
||||
|
||||
const { autumnV1, autumnV2_2, ctx } = await initScenario({
|
||||
customerId,
|
||||
setup: [
|
||||
s.customer({ testClock: true, paymentMethod: "success" }),
|
||||
s.products({ list: [pro, premium] }),
|
||||
],
|
||||
actions: [s.billing.attach({ productId: pro.id })],
|
||||
});
|
||||
|
||||
const beforeFullCustomer = await CusService.getFull({
|
||||
ctx,
|
||||
idOrInternalId: customerId,
|
||||
});
|
||||
const beforeProCusProduct = beforeFullCustomer.customer_products.find(
|
||||
(cp) => cp.product.id === pro.id,
|
||||
);
|
||||
const expectedSubscriptionIds = beforeProCusProduct?.subscription_ids ?? [];
|
||||
expect(expectedSubscriptionIds.length).toBeGreaterThan(0);
|
||||
|
||||
await autumnV2_2.billing.attach<AttachParamsV1Input>({
|
||||
customer_id: customerId,
|
||||
plan_id: premium.id,
|
||||
no_billing_changes: true,
|
||||
});
|
||||
|
||||
const afterCustomer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||
await expectCustomerProducts({
|
||||
customer: afterCustomer,
|
||||
active: [premium.id],
|
||||
notPresent: [pro.id],
|
||||
});
|
||||
|
||||
const afterFullCustomer = await CusService.getFull({
|
||||
ctx,
|
||||
idOrInternalId: customerId,
|
||||
});
|
||||
const afterPremiumCusProduct = afterFullCustomer.customer_products.find(
|
||||
(cp) => cp.product.id === premium.id,
|
||||
);
|
||||
|
||||
expect(afterPremiumCusProduct?.subscription_ids).toEqual(
|
||||
expectedSubscriptionIds,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -44,10 +44,7 @@
|
||||
"drizzle-orm/*": ["../node_modules/drizzle-orm/*"],
|
||||
|
||||
"@better-auth/core": ["../node_modules/@better-auth/core"],
|
||||
"@better-auth/core/*": ["../node_modules/@better-auth/core/*"],
|
||||
|
||||
"hono": ["../node_modules/hono"],
|
||||
"hono/*": ["../node_modules/hono/*"]
|
||||
"@better-auth/core/*": ["../node_modules/@better-auth/core/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "tests", "scripts", "experiments", "perf"],
|
||||
|
||||
Reference in New Issue
Block a user