Skip to content
Merged

Main #3215

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ const executeCheckoutSessionMetadataV2 = async ({
billingContext: updatedDeferredData.billingContext,
});

await promotePendingCustomerProducts({
const autumnBillingPlanToExecute = await promotePendingCustomerProducts({
ctx,
autumnBillingPlan: updatedDeferredData.billingPlan.autumn,
fullCustomer: updatedDeferredData.billingContext.fullCustomer,
Expand All @@ -176,7 +176,7 @@ const executeCheckoutSessionMetadataV2 = async ({
// Execute autumn billing plan (includes customer products, upsertSubscription, upsertInvoice)
await executeAutumnBillingPlan({
ctx,
autumnBillingPlan: updatedDeferredData.billingPlan.autumn,
autumnBillingPlan: autumnBillingPlanToExecute,
stripeInvoice: checkoutContext.stripeInvoice,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const executeDeferredBillingPlan = async ({
});
}

await promotePendingCustomerProducts({
const autumnBillingPlanToExecute = await promotePendingCustomerProducts({
ctx,
autumnBillingPlan: billingPlan.autumn,
fullCustomer: billingContext.fullCustomer,
Expand All @@ -67,7 +67,7 @@ export const executeDeferredBillingPlan = async ({

await executeAutumnBillingPlan({
ctx,
autumnBillingPlan: billingPlan.autumn,
autumnBillingPlan: autumnBillingPlanToExecute,
stripeInvoice: stripeBillingResult.stripeInvoice ?? stripeInvoice,
stripeInvoiceItems: stripeBillingResult.stripeInvoiceItems,
autumnInvoice: stripeBillingResult.autumnInvoice,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const promotePendingCustomerProducts = async ({
inStatuses: [CusProductStatus.Pending],
});

if (!pendingCustomerProducts.length) return;
if (!pendingCustomerProducts.length) return autumnBillingPlan;

const promotedIds = new Set<string>();

Expand Down Expand Up @@ -59,8 +59,11 @@ export const promotePendingCustomerProducts = async ({
promotedIds.add(customerProduct.id);
}

autumnBillingPlan.insertCustomerProducts =
autumnBillingPlan.insertCustomerProducts?.filter(
(planned) => !promotedIds.has(planned.id),
) ?? [];
return {
...autumnBillingPlan,
insertCustomerProducts:
autumnBillingPlan.insertCustomerProducts?.filter(
(planned) => !promotedIds.has(planned.id),
) ?? [],
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ test.concurrent(
// CHECKOUT: ATTACH VIA STRIPE CHECKOUT (no payment method on file)
// ═══════════════════════════════════════════════════════════════════════════════

// Red: pending promotion erased the paid plan before billing and product webhooks.
// Green: checkout emits both events with the activated paid plan.
test(`${chalk.yellowBright("billing.updated: stripe checkout completion → activated")}`, async () => {
const customerId = "billing-updated-stripe-checkout";
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
Expand All @@ -478,18 +480,30 @@ test(`${chalk.yellowBright("billing.updated: stripe checkout completion → acti
// handleCheckoutSessionMetadataV2 → executeBillingPlan → webhook fires
await completeStripeCheckoutForm({ url: attachResult.payment_url });

const result = await waitForWebhook<BillingUpdatedPayload>({
token: playToken,
predicate: (payload) =>
payload.type === "billing.updated" &&
payload.data?.customer_id === customerId &&
findChange(payload.data.plan_changes, {
action: "activated",
planId: pro.id,
}) !== undefined,
timeoutMs: 30000,
});
const [productsResult, result] = await Promise.all([
waitForWebhook<CustomerProductsUpdatedPayload>({
token: playToken,
predicate: (payload) =>
payload.type === "customer.products.updated" &&
payload.data?.customer?.id === customerId &&
payload.data?.updated_product?.id === pro.id &&
payload.data?.scenario === "new",
timeoutMs: 30000,
}),
waitForWebhook<BillingUpdatedPayload>({
token: playToken,
predicate: (payload) =>
payload.type === "billing.updated" &&
payload.data?.customer_id === customerId &&
findChange(payload.data.plan_changes, {
action: "activated",
planId: pro.id,
}) !== undefined,
timeoutMs: 30000,
}),
]);

expect(productsResult).not.toBeNull();
expect(result).not.toBeNull();
const activated = findChange(result!.payload.data.plan_changes, {
action: "activated",
Expand Down
Loading