diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index adecff0f8..1659d192c 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -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); - await stripeCli.invoices.voidInvoice(subscription.latest_invoice); + 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 }); diff --git a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts index ddeeb2a18..9d1773225 100644 --- a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts +++ b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts @@ -26,7 +26,6 @@ export const createAndFinalizeInvoice = async ({ customer: stripeCusId, auto_advance: false, subscription: stripeSubId, - collection_method: chargeAutomatically ? "charge_automatically" : "send_invoice", diff --git a/server/tests/_temp/temp1.test.ts b/server/tests/_temp/temp1.test.ts index 7823b3bd5..0df9c1b62 100644 --- a/server/tests/_temp/temp1.test.ts +++ b/server/tests/_temp/temp1.test.ts @@ -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, }); }); diff --git a/server/tests/_temp/temp2.test.ts b/server/tests/_temp/temp2.test.ts new file mode 100644 index 000000000..15947fc06 --- /dev/null +++ b/server/tests/_temp/temp2.test.ts @@ -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, + // }); + // }); +});