fix: update quantity cancellation bug fix

This commit is contained in:
Charlie Lamb
2025-12-18 13:54:28 +00:00
parent b823a1f0c9
commit ad196f4fa0
9 changed files with 59 additions and 40 deletions

View File

@@ -313,7 +313,16 @@ export const getStripeProrationBehavior = ({
: behaviourMap[ProrationBehavior.NextBilling];
};
export const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => {
/**
* Checks if a Stripe subscription is canceled.
* @param sub - The Stripe subscription to check.
* @returns True if the subscription is canceled, false otherwise.
*/
export const isStripeSubscriptionCanceled = ({
sub,
}: {
sub: Stripe.Subscription;
}) => {
return (
notNullish(sub.canceled_at) ||
notNullish(sub.cancel_at) ||

View File

@@ -11,7 +11,7 @@ import {
import type Stripe from "stripe";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import { isStripeSubscriptionCanceled } from "@/external/stripe/stripeSubUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
@@ -130,7 +130,7 @@ export const handlePaidProduct = async ({
});
}
if (subIsCanceled({ sub: mergeSub })) {
if (isStripeSubscriptionCanceled({ sub: mergeSub })) {
logger.info("ADD PRODUCT FLOW, CREATING NEW SCHEDULE");
schedule = await subToNewSchedule({
ctx,

View File

@@ -4,7 +4,10 @@ import {
SuccessCode,
} from "@autumn/shared";
import type Stripe from "stripe";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import {
getStripeSubs,
isStripeSubscriptionCanceled,
} from "@/external/stripe/stripeSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../../cusProducts/AttachParams.js";
@@ -22,7 +25,7 @@ export const handleUpdateQuantityFunction = async ({
}) => {
const { db } = ctx;
// 2. Update quantities
// Update quantities
const optionsToUpdate = attachParams.optionsToUpdate!;
const { curSameProduct } = attachParamToCusProducts({ attachParams });
@@ -52,10 +55,23 @@ export const handleUpdateQuantityFunction = async ({
}
}
for (const stripeSub of stripeSubs) {
if (isStripeSubscriptionCanceled({ sub: stripeSub })) {
stripeCli.subscriptions.update(stripeSub.id, {
cancel_at: null,
});
}
}
await CusProductService.update({
db,
cusProductId: cusProduct.id,
updates: { options: optionsToUpdate.map((o) => o.new) },
updates: {
options: optionsToUpdate.map((o) => o.new),
canceled_at: null,
canceled: false,
ended_at: null,
},
});
return AttachFunctionResponseSchema.parse({
@@ -64,15 +80,4 @@ export const handleUpdateQuantityFunction = async ({
invoice:
config.invoiceOnly && invoices.length > 0 ? invoices[0] : undefined,
});
// res.status(200).json(
// AttachResultSchema.parse({
// customer_id: customer.id || customer.internal_id,
// product_ids: attachParams.products.map((p) => p.id),
// invoice:
// config.invoiceOnly && invoices.length > 0 ? invoices[0] : undefined,
// code: SuccessCode.FeaturesUpdated,
// message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`,
// }),
// );
};

View File

@@ -11,7 +11,7 @@ import {
import type Stripe from "stripe";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import { isStripeSubscriptionCanceled } from "@/external/stripe/stripeSubUtils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
@@ -113,6 +113,7 @@ export const handleUpgradeFlow = async ({
);
canceled = true;
const { stripeCli } = attachParams;
await stripeCli.subscriptions.cancel(curSub.id, {
prorate: config.proration === ProrationBehavior.Immediately,
invoice_now: config.proration === ProrationBehavior.Immediately,
@@ -135,8 +136,8 @@ export const handleUpgradeFlow = async ({
});
// // Renew sub
// console.log("Sub is canceled!", subIsCanceled({ sub: res.updatedSub }));
// if (subIsCanceled({ sub: res.updatedSub })) {
// console.log("Sub is canceled!", isStripeSubscriptionCanceled({ sub: res.updatedSub }));
// if (isStripeSubscriptionCanceled({ sub: res.updatedSub })) {
// await attachParams.stripeCli.subscriptions.update(res.updatedSub.id, {
// cancel_at_period_end: false,
// cancel_at: null,
@@ -211,7 +212,7 @@ export const handleUpgradeFlow = async ({
const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined;
let canceledAt: number | undefined;
if (sub && subIsCanceled({ sub })) {
if (sub && isStripeSubscriptionCanceled({ sub })) {
canceledAt = sub.canceled_at
? sub.canceled_at * 1000
: curCusProduct?.canceled_at || undefined;

View File

@@ -13,7 +13,7 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
import { SubService } from "@/internal/subscriptions/SubService.js";
import { nullish } from "@/utils/genUtils.js";
import type { ItemSet } from "@/utils/models/ItemSet.js";
import { subIsCanceled } from "../../../../../external/stripe/stripeSubUtils.js";
import { isStripeSubscriptionCanceled } from "../../../../../external/stripe/stripeSubUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js";
@@ -91,7 +91,9 @@ export const updateStripeSub2 = async ({
// cancel_at_period_end: false,
// TODO: will error if sub managed by a schedule
cancel_at_period_end: subIsCanceled({ sub: curSub }) ? false : undefined,
cancel_at_period_end: isStripeSubscriptionCanceled({ sub: curSub })
? false
: undefined,
});
let latestInvoice = updatedSub.latest_invoice as Stripe.Invoice | null;

View File

@@ -19,7 +19,7 @@ export const handleOAuthCallback = async (c: Context<HonoEnv>) => {
const { db } = initDrizzle();
// Build frontend redirect URL (default)
const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173";
const frontendUrl = process.env.CLIENT_URL || "http://localhost:3000";
let redirectUrl = new URL(`${frontendUrl}`);
redirectUrl.searchParams.set("tab", "stripe");

View File

@@ -13,7 +13,7 @@ import {
} from "@autumn/shared";
import type { DrizzleCli } from "@server/db/initDrizzle";
import { priceToStripeItem } from "@server/external/stripe/priceToStripeItem/priceToStripeItem";
import { subIsCanceled } from "@server/external/stripe/stripeSubUtils";
import { isStripeSubscriptionCanceled } from "@server/external/stripe/stripeSubUtils";
import {
cusProductInPhase,
logPhaseItems,
@@ -163,7 +163,6 @@ const compareActualItems = async ({
continue;
}
const { autumnPrice: _, ...rest } = expectedItem;
console.log(`(${type}) Missing item:`, rest);
@@ -255,7 +254,6 @@ const compareActualItems = async ({
}
};
export const checkCusSubCorrect = async ({
db,
fullCus,
@@ -271,8 +269,6 @@ export const checkCusSubCorrect = async ({
org: Organization;
env: AppEnv;
}) => {
// 1. Only 1 sub ID available
const cusProducts = fullCus.customer_products;
const subIds = cusProductToSubIds({ cusProducts });
@@ -489,7 +485,6 @@ export const checkCusSubCorrect = async ({
}
assert(!!sub, `Sub ${subId} should exist`);
if (sub) {
const actualItems = sub!.items.data.map((item: any) => ({
@@ -508,7 +503,6 @@ export const checkCusSubCorrect = async ({
subId,
});
}
// Should be canceled
@@ -540,11 +534,12 @@ export const checkCusSubCorrect = async ({
const finalShouldBeCanceled = cusSubShouldBeCanceled;
if (finalShouldBeCanceled) {
assert(!sub!.schedule, `sub ${subId} should NOT have a schedule`);
assert(subIsCanceled({ sub: sub! }), `sub ${subId} should be canceled`);
assert(
isStripeSubscriptionCanceled({ sub: sub! }),
`sub ${subId} should be canceled`,
);
continue;
}
@@ -553,7 +548,6 @@ export const checkCusSubCorrect = async ({
? schedules.find((s) => s.id === sub!.schedule)
: null;
for (let i = 0; i < supposedPhases.length; i++) {
const supposedPhase = supposedPhases[i];

View File

@@ -14,7 +14,7 @@ import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js";
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import { isStripeSubscriptionCanceled } from "@/external/stripe/stripeSubUtils.js";
import {
cusProductInPhase,
logPhaseItems,
@@ -430,7 +430,7 @@ export const expectSubToBeCorrect = async ({
if (finalShouldBeCanceled) {
expect(sub.schedule).toBeNull();
// expect(sub.cancel_at).toBeDefined();
expect(subIsCanceled({ sub })).toBe(true);
expect(isStripeSubscriptionCanceled({ sub })).toBe(true);
return;
}

View File

@@ -70,7 +70,11 @@ export const getSubsFromCusId = async ({
};
};
const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => {
const isStripeSubscriptionCanceled = ({
sub,
}: {
sub: Stripe.Subscription;
}) => {
return (
notNullish(sub.canceled_at) ||
notNullish(sub.cancel_at) ||
@@ -155,9 +159,13 @@ export const expectSubItemsCorrect = async ({
for (const sub of subs) {
if (subCanceled) {
expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
expect(isStripeSubscriptionCanceled({ sub }), "sub should be canceled").to
.be.true;
} else {
expect(subIsCanceled({ sub }), "sub should not be canceled").to.be.false;
expect(
isStripeSubscriptionCanceled({ sub }),
"sub should not be canceled",
).to.be.false;
}
}