fix: update quantity cancellation bug fix
This commit is contained in:
11
server/src/external/stripe/stripeSubUtils.ts
vendored
11
server/src/external/stripe/stripeSubUtils.ts
vendored
@@ -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) ||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(", ")}`,
|
||||
// }),
|
||||
// );
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user