fix: going into past due

This commit is contained in:
John Yeo
2025-11-16 11:55:35 +00:00
parent fa9f024c82
commit 1d7c490ad4
4 changed files with 118 additions and 61 deletions

View File

@@ -117,6 +117,7 @@ export const handleSubscriptionUpdated = async ({
}
// Cancel subscription immediately
if (subscription.status === "past_due" && org.config.cancel_on_past_due) {
const stripeCli = createStripeCli({
org,
@@ -130,14 +131,11 @@ export const handleSubscriptionUpdated = async ({
logger.info(
`Latest invoice billing reason: ${latestInvoice.billing_reason}`,
);
logger.info(`Latest invoice status: ${latestInvoice.status}`);
if (
latestInvoice.status !== "open" ||
latestInvoice.billing_reason !== "subscription_cycle"
) {
const validInvoiceReasons = ["subscription_cycle", "subscription_create"];
if (!validInvoiceReasons.includes(latestInvoice.billing_reason ?? "")) {
logger.info(
"sub.updated, latest invoice isn't open or billing reason isn't subscription_update, past_due not forcing cancel",
"sub.updated, latest invoice billing reason isn't subscription_cycle / subscription_create, past_due not forcing cancel",
{
data: {
subscriptionId: subscription.id,
@@ -165,7 +163,9 @@ export const handleSubscriptionUpdated = async ({
},
);
await stripeCli.subscriptions.cancel(subscription.id);
if (latestInvoice.status === "open") {
await stripeCli.invoices.voidInvoice(subscription.latest_invoice);
}
} catch (error: any) {
logger.error(
`subscription.updated: error cancelling / voiding: ${error.message}`,
@@ -183,48 +183,3 @@ export const handleSubscriptionUpdated = async ({
}
}
};
// server-1 | subscription.updated, previous attributes: { status: 'active' }
// server-1 | current period start: 8 Jul 2025
// server-1 | current period end: 8 Aug 2025
// server-1 | subscription.updated: past due, cancelling: sub_1Rig399mx3u0jkgOquNmw5fM
// server-1 | subscription.updated, previous attributes: { status: 'active' }
// server-1 | current period start: 8 Aug 2025
// server-1 | current period end: 8 Sep 2025
// server-1 | subscription.updated: past due, cancelling: sub_1Rig3z9mx3u0jkgOzJTci91r
// const lockKey = `sub_updated_${subscription.id}`;
// // Create a lock to prevent race conditions
// let lockAcquired = false;
// try {
// let attempts = 0;
// while (!lockAcquired && attempts < 3) {
// lockAcquired = await getWebhookLock({ lockKey, logger });
// if (!lockAcquired) {
// attempts++;
// console.log(
// `sub.updated: failed to acquire lock for ${subscription.id}, attempt ${attempts}`,
// );
// if (attempts < 3) {
// await new Promise((resolve) => setTimeout(resolve, 1000));
// }
// } else {
// break;
// }
// }
// } catch (error) {
// logger.error("lock error, setting lockAcquired to true");
// lockAcquired = true;
// }
// if (!lockAcquired) {
// throw new RecaseError({
// message: `Failed to acquire lock for stripe webhook, sub.updated.`,
// code: ErrCode.InvalidRequest,
// statusCode: 400,
// });
// }
// await releaseWebhookLock({ lockKey, logger });

View File

@@ -26,7 +26,6 @@ export const createAndFinalizeInvoice = async ({
customer: stripeCusId,
auto_advance: false,
subscription: stripeSubId,
collection_method: chargeAutomatically
? "charge_automatically"
: "send_invoice",

View File

@@ -1,5 +1,5 @@
import { beforeAll, describe, test } from "bun:test";
import { FreeTrialDuration, LegacyVersion } from "@autumn/shared";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
@@ -8,6 +8,7 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceToNextInvoice } from "../utils/testAttachUtils/testAttachUtils.js";
// UNCOMMENT FROM HERE
const pro = constructProduct({
@@ -16,12 +17,6 @@ const pro = constructProduct({
items: [
constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 100 }),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: false,
card_required: true,
},
});
describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
@@ -34,7 +29,7 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
ctx,
customerId,
customerData: {},
attachPm: "success",
attachPm: "fail",
withTestClock: true,
});
@@ -51,6 +46,13 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
invoice: true,
enable_product_immediately: true,
});
await advanceToNextInvoice({
stripeCli: ctx.stripeCli,
testClockId,
});
});

View File

@@ -0,0 +1,101 @@
import { beforeAll, describe, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { attachFailedPaymentMethod } from "../../src/external/stripe/stripeCusUtils.js";
import { CusService } from "../../src/internal/customers/CusService.js";
// UNCOMMENT FROM HERE
const pro = constructProduct({
type: "pro",
items: [
constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 10,
includedUsage: 0,
}),
],
});
describe(`${chalk.yellowBright("temp: Testing pay per use")}`, () => {
const customerId = "temp2";
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
beforeAll(async () => {
const result = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: customerId,
});
testClockId = result.testClockId!;
});
test("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
// await advanceToNextInvoice({
// stripeCli: ctx.stripeCli,
// testClockId,
// });
const customer = await CusService.get({
db: ctx.db,
idOrInternalId: customerId,
orgId: ctx.org.id,
env: ctx.env,
});
await attachFailedPaymentMethod({
stripeCli: ctx.stripeCli,
customer: customer!,
});
// await autumn.track({
// customer_id: customerId,
// feature_id: TestFeature.Users,
// value: 1,
// });
await autumn.entities.create(customerId, [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
]);
});
// test("should cancel one add on", async () => {
// await autumn.cancel({
// customer_id: customerId,
// product_id: addOn.id,
// });
// await expectSubToBeCorrect({
// customerId,
// db,
// org,
// env,
// });
// });
});