feat: payment failure returns checkout_url
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
import { initializeDatabaseFunctions } from "@server/db/initializeDatabaseFunctions";
|
||||
import inquirer from "inquirer";
|
||||
loadLocalEnv();
|
||||
|
||||
import { loadLocalEnv } from "@server/utils/envUtils";
|
||||
import inquirer from "inquirer";
|
||||
export const migrateFunctions = async () => {
|
||||
// Dynamic import to ensure env is loaded first
|
||||
const { initializeDatabaseFunctions } = await import(
|
||||
"@server/db/initializeDatabaseFunctions"
|
||||
);
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
console.log("databaseUrl", databaseUrl);
|
||||
if (databaseUrl?.includes("us-west-3")) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
|
||||
@@ -11,4 +11,5 @@ BUN_PARALLEL_COMPACT \
|
||||
'server/tests/attach/addOn' \
|
||||
'server/tests/attach/checkout' \
|
||||
'server/tests/attach/misc' \
|
||||
'server/tests/billing/invoice-action-required' \
|
||||
--max=6 \
|
||||
|
||||
@@ -4,13 +4,16 @@ import { UTCDate } from "@date-fns/utc";
|
||||
import { CronJob } from "cron";
|
||||
import { format } from "date-fns";
|
||||
import { initDrizzle } from "../db/initDrizzle.js";
|
||||
import { logger } from "../external/logtail/logtailUtils.js";
|
||||
import { CusEntService } from "../internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { notNullish } from "../utils/genUtils.js";
|
||||
import {
|
||||
clearCusEntsFromCache,
|
||||
resetCustomerEntitlement,
|
||||
} from "./cronUtils.js";
|
||||
import { runInvoiceCron } from "./invoiceCron/runInvoiceCron.js";
|
||||
import { runProductCron } from "./productCron/runProductCron.js";
|
||||
import type { CronContext } from "./utils/CronContext.js";
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
@@ -66,7 +69,17 @@ const main = async () => {
|
||||
console.log(`Cron disabled!`);
|
||||
return;
|
||||
}
|
||||
await Promise.all([cronTask(), runProductCron()]);
|
||||
|
||||
const ctx: CronContext = {
|
||||
db,
|
||||
logger,
|
||||
};
|
||||
await Promise.all([
|
||||
cronTask(),
|
||||
runProductCron(),
|
||||
runInvoiceCron({ ctx }),
|
||||
// TODO: Add runUsageCron({ ctx })
|
||||
]);
|
||||
};
|
||||
|
||||
new CronJob(
|
||||
|
||||
65
server/src/cron/invoiceCron/runInvoiceCron.ts
Normal file
65
server/src/cron/invoiceCron/runInvoiceCron.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { type Metadata, MetadataType, metadata } from "@autumn/shared";
|
||||
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import { createStripeCli } from "../../external/connect/createStripeCli";
|
||||
import type { AttachParams } from "../../internal/customers/cusProducts/AttachParams";
|
||||
import type { CronContext } from "../utils/CronContext";
|
||||
|
||||
export const handleVoidInvoiceCron = async ({
|
||||
ctx,
|
||||
metadata,
|
||||
}: {
|
||||
ctx: CronContext;
|
||||
metadata: Metadata;
|
||||
}) => {
|
||||
const { logger } = ctx;
|
||||
const data = metadata.data as AttachParams;
|
||||
const { org, customer } = data;
|
||||
const stripeCli = createStripeCli({ org, env: customer.env });
|
||||
|
||||
if (!metadata.stripe_invoice_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const invoice = await stripeCli.invoices.retrieve(metadata.stripe_invoice_id);
|
||||
if (invoice.status === "open") {
|
||||
try {
|
||||
await stripeCli.invoices.voidInvoice(metadata.stripe_invoice_id);
|
||||
logger.info(
|
||||
`voided invoice ${metadata.stripe_invoice_id} for customer ${customer.id} (org: ${org.slug})`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Error voiding invoice: ${error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const runInvoiceCron = async ({ ctx }: { ctx: CronContext }) => {
|
||||
console.log("Running invoice cron");
|
||||
const { db } = ctx;
|
||||
|
||||
// 1. Fetch from metadata invoices
|
||||
const invoices = await db
|
||||
.select()
|
||||
.from(metadata)
|
||||
.where(
|
||||
and(
|
||||
eq(metadata.type, MetadataType.InvoiceActionRequired),
|
||||
lt(metadata.expires_at, Date.now()),
|
||||
),
|
||||
);
|
||||
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < invoices.length; i += batchSize) {
|
||||
const batch = invoices.slice(i, i + batchSize);
|
||||
|
||||
const promises = [];
|
||||
for (const metadata of batch) {
|
||||
promises.push(handleVoidInvoiceCron({ ctx, metadata }));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
console.log(`Handled ${i + batch.length}/${invoices.length} invoices`);
|
||||
console.log("----------------------------------\n");
|
||||
}
|
||||
console.log("FINISHED INVOICE CRON");
|
||||
};
|
||||
7
server/src/cron/utils/CronContext.ts
Normal file
7
server/src/cron/utils/CronContext.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { DrizzleCli } from "../../db/initDrizzle";
|
||||
import type { Logger } from "../../external/logtail/logtailUtils";
|
||||
|
||||
export interface CronContext {
|
||||
db: DrizzleCli;
|
||||
logger: Logger;
|
||||
}
|
||||
@@ -203,12 +203,9 @@ export const handleStripeWebhookEvent = async ({
|
||||
case "invoice.paid": {
|
||||
const invoice = event.data.object;
|
||||
await handleInvoicePaid({
|
||||
db,
|
||||
org,
|
||||
ctx,
|
||||
invoiceData: invoice,
|
||||
env,
|
||||
event,
|
||||
req: ctx as unknown as ExtendedRequest,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
33
server/src/external/stripe/stripeCusUtils.ts
vendored
33
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -11,6 +11,7 @@ import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
|
||||
|
||||
export const getStripeCus = async ({
|
||||
stripeCli,
|
||||
@@ -313,6 +314,38 @@ export const attachFailedPaymentMethod = async ({
|
||||
});
|
||||
};
|
||||
|
||||
export const attachAuthenticatePaymentMethod = async ({
|
||||
ctx,
|
||||
customerId,
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
customerId: string;
|
||||
}) => {
|
||||
const { org, env, db } = ctx;
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
const autumnCustomer = await CusService.get({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: org.id,
|
||||
env: env,
|
||||
});
|
||||
|
||||
const stripeCustomer = await stripeCli.customers.retrieve(
|
||||
autumnCustomer!.processor?.id,
|
||||
);
|
||||
// Delete existing payment method
|
||||
const paymentMethods = await stripeCli.paymentMethods.list({
|
||||
customer: stripeCustomer.id,
|
||||
});
|
||||
for (const pm of paymentMethods.data) {
|
||||
await stripeCli.paymentMethods.detach(pm.id);
|
||||
}
|
||||
|
||||
await stripeCli.paymentMethods.attach("pm_card_authenticationRequired", {
|
||||
customer: stripeCustomer.id,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAllStripeCustomers = async ({
|
||||
org,
|
||||
env,
|
||||
|
||||
@@ -114,7 +114,11 @@ export const payForInvoice = async ({
|
||||
}
|
||||
|
||||
if (errorOnFail) {
|
||||
throw error;
|
||||
throw new RecaseError({
|
||||
message: error?.message,
|
||||
code: ErrCode.PayInvoiceFailed,
|
||||
data: invoice,
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
paid: false,
|
||||
@@ -122,7 +126,7 @@ export const payForInvoice = async ({
|
||||
message: `Failed to pay invoice: ${error?.message || error}`,
|
||||
code: ErrCode.PayInvoiceFailed,
|
||||
}),
|
||||
invoice: null,
|
||||
invoice: invoice,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { InternalError, MetadataType } from "@autumn/shared";
|
||||
import { addMinutes } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
|
||||
import { attachParamsToMetadata } from "../../../../internal/billing/attach/utils/attachParamsToMetadata.js";
|
||||
import type { Logger } from "../../../logtail/logtailUtils.js";
|
||||
import { payForInvoice } from "../../stripeInvoiceUtils.js";
|
||||
|
||||
export const undoSubUpdate = async ({
|
||||
@@ -55,17 +58,19 @@ export const undoSubUpdate = async ({
|
||||
};
|
||||
|
||||
export const createProrationInvoice = async ({
|
||||
ctx,
|
||||
attachParams,
|
||||
invoiceOnly,
|
||||
curSub,
|
||||
updatedSub,
|
||||
logger,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
attachParams: AttachParams;
|
||||
invoiceOnly: boolean;
|
||||
curSub: Stripe.Subscription;
|
||||
updatedSub: Stripe.Subscription;
|
||||
logger: any;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
const { stripeCli, customer, paymentMethod } = attachParams;
|
||||
|
||||
@@ -77,49 +82,78 @@ export const createProrationInvoice = async ({
|
||||
|
||||
if (items.data.length === 0) {
|
||||
logger.info(`No items to prorate, skipping invoice creation`);
|
||||
return null;
|
||||
return {
|
||||
invoice: null,
|
||||
url: null,
|
||||
};
|
||||
}
|
||||
|
||||
// const shouldMemo = attachParams.org.config.invoice_memos && invoiceOnly;
|
||||
// const invoiceMemo = shouldMemo
|
||||
// ? await buildInvoiceMemoFromEntitlements({
|
||||
// org: attachParams.org,
|
||||
// entitlements: attachParams.entitlements,
|
||||
// features: attachParams.features,
|
||||
// })
|
||||
// : undefined;
|
||||
|
||||
const invoice = await stripeCli.invoices.create({
|
||||
customer: customer.processor.id,
|
||||
subscription: curSub.id,
|
||||
// subscription: curSub.id,
|
||||
auto_advance: false,
|
||||
// ...(shouldMemo ? { description: invoiceMemo } : {}),
|
||||
pending_invoice_items_behavior: "include",
|
||||
});
|
||||
|
||||
if (invoiceOnly) return invoice;
|
||||
if (invoiceOnly)
|
||||
return {
|
||||
invoice,
|
||||
url: null,
|
||||
};
|
||||
|
||||
await stripeCli.invoices.finalizeInvoice(invoice.id!, {
|
||||
auto_advance: false,
|
||||
});
|
||||
|
||||
try {
|
||||
const { invoice: subInvoice } = await payForInvoice({
|
||||
const {
|
||||
paid,
|
||||
error,
|
||||
invoice: subInvoice,
|
||||
} = await payForInvoice({
|
||||
stripeCli,
|
||||
paymentMethod: paymentMethod || null,
|
||||
invoiceId: invoice.id!,
|
||||
logger,
|
||||
voidIfFailed: true,
|
||||
voidIfFailed: false,
|
||||
errorOnFail: false,
|
||||
});
|
||||
|
||||
return subInvoice;
|
||||
} catch (error: any) {
|
||||
if (!paid) {
|
||||
await undoSubUpdate({ stripeCli, curSub, updatedSub });
|
||||
|
||||
throw new RecaseError({
|
||||
code: ErrCode.UpdateSubscriptionFailed,
|
||||
message: `Failed to update subscription. ${error.message}`,
|
||||
if (subInvoice && subInvoice.status === "open") {
|
||||
logger.info(
|
||||
`[update subscription] invoice action required: ${subInvoice.id}`,
|
||||
);
|
||||
const metadata = await attachParamsToMetadata({
|
||||
db: ctx.db,
|
||||
attachParams,
|
||||
type: MetadataType.InvoiceActionRequired,
|
||||
stripeInvoiceId: subInvoice.id,
|
||||
expiresAt: addMinutes(Date.now(), 10).getTime(),
|
||||
});
|
||||
|
||||
await stripeCli.invoices.update(subInvoice.id, {
|
||||
metadata: {
|
||||
autumn_metadata_id: metadata.id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
invoice: subInvoice,
|
||||
url: subInvoice?.hosted_invoice_url,
|
||||
};
|
||||
} else {
|
||||
throw new InternalError({
|
||||
message: `[update subscription] Failed to pay invoice: ${error?.message}`,
|
||||
code: "update_subscription_failed",
|
||||
statusCode: 500,
|
||||
data: `Stripe error: ${error.message}`,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
invoice: subInvoice,
|
||||
url: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
|
||||
// Get options
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
const attachParams: AttachParams = metadata.data;
|
||||
const attachParams: AttachParams = metadata.data as AttachParams;
|
||||
const checkoutSession = await stripeCli.checkout.sessions.retrieve(data.id, {
|
||||
expand: ["line_items", "subscription"],
|
||||
});
|
||||
|
||||
@@ -8,13 +8,13 @@ import type {
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
|
||||
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
|
||||
import {
|
||||
getFullStripeInvoice,
|
||||
getInvoiceDiscounts,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../stripeInvoiceUtils.js";
|
||||
import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js";
|
||||
import { getStripeSubs } from "../stripeSubUtils.js";
|
||||
import { handleInvoicePaidMetadata } from "./handleInvoicePaid/handleInvoicePaidMetadata.js";
|
||||
import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
|
||||
|
||||
const handleOneOffInvoicePaid = async ({
|
||||
@@ -137,21 +138,15 @@ const convertToChargeAutomatically = async ({
|
||||
};
|
||||
|
||||
export const handleInvoicePaid = async ({
|
||||
db,
|
||||
req,
|
||||
org,
|
||||
ctx,
|
||||
invoiceData,
|
||||
env,
|
||||
event,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
req: any;
|
||||
org: Organization;
|
||||
ctx: AutumnContext;
|
||||
invoiceData: Stripe.Invoice;
|
||||
env: AppEnv;
|
||||
event: Stripe.Event;
|
||||
}) => {
|
||||
const logger = req.logger;
|
||||
const { logger, org, env, db } = ctx;
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
const invoice = await getFullStripeInvoice({
|
||||
stripeCli,
|
||||
@@ -160,12 +155,8 @@ export const handleInvoicePaid = async ({
|
||||
});
|
||||
|
||||
if (invoice.metadata?.autumn_metadata_id) {
|
||||
await handleInvoiceCheckoutPaid({
|
||||
req,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
stripeCli,
|
||||
await handleInvoicePaidMetadata({
|
||||
ctx,
|
||||
invoice,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { AttachBranch, type Metadata, ProrationBehavior } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
|
||||
import { resetUsageBalances } from "../../../../internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems";
|
||||
import { handleUpgradeFlow } from "../../../../internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow";
|
||||
import { attachParamToCusProducts } from "../../../../internal/customers/attach/attachUtils/convertAttachParams";
|
||||
import { getDefaultAttachConfig } from "../../../../internal/customers/attach/attachUtils/getAttachConfig";
|
||||
import type { AttachParams } from "../../../../internal/customers/cusProducts/AttachParams";
|
||||
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer";
|
||||
import { MetadataService } from "../../../../internal/metadata/MetadataService";
|
||||
import { createStripeCli } from "../../../connect/createStripeCli";
|
||||
import { getCusPaymentMethod } from "../../stripeCusUtils";
|
||||
|
||||
export const handleInvoiceActionRequiredCompleted = async ({
|
||||
ctx,
|
||||
invoice,
|
||||
metadata,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
invoice: Stripe.Invoice;
|
||||
metadata: Metadata;
|
||||
}) => {
|
||||
const { logger, org, env } = ctx;
|
||||
logger.info(`invoice.paid, handling action required`);
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
const paymentMethod = await getCusPaymentMethod({
|
||||
stripeCli,
|
||||
stripeId: invoice.customer as string,
|
||||
});
|
||||
|
||||
const attachParams = {
|
||||
...(metadata.data as AttachParams),
|
||||
stripeCli,
|
||||
req: ctx,
|
||||
paymentMethod,
|
||||
} as AttachParams;
|
||||
|
||||
const attachConfig = {
|
||||
...getDefaultAttachConfig(),
|
||||
proration: ProrationBehavior.None,
|
||||
};
|
||||
|
||||
ctx.logger.info(`handling upgrade flow for invoice ${invoice.id}`);
|
||||
|
||||
const { curMainProduct } = attachParamToCusProducts({ attachParams });
|
||||
|
||||
await handleUpgradeFlow({
|
||||
ctx,
|
||||
attachParams,
|
||||
config: attachConfig,
|
||||
branch: AttachBranch.Upgrade,
|
||||
});
|
||||
|
||||
if (attachParams.cusEntIds && curMainProduct) {
|
||||
await resetUsageBalances({
|
||||
db: ctx.db,
|
||||
cusEntIds: attachParams.cusEntIds,
|
||||
cusProduct: curMainProduct,
|
||||
});
|
||||
}
|
||||
|
||||
await MetadataService.delete({
|
||||
db: ctx.db,
|
||||
id: metadata.id,
|
||||
});
|
||||
|
||||
await deleteCachedApiCustomer({
|
||||
customerId: attachParams.customer.id || "",
|
||||
orgId: attachParams.org.id,
|
||||
env: attachParams.customer.env,
|
||||
});
|
||||
};
|
||||
56
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoiceCheckoutPaid.ts
vendored
Normal file
56
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoiceCheckoutPaid.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Metadata } from "@autumn/shared";
|
||||
import { AttachScenario } from "@autumn/shared";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
|
||||
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
|
||||
|
||||
export const handleInvoiceCheckoutPaid = async ({
|
||||
ctx,
|
||||
metadata,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
metadata: Metadata;
|
||||
}) => {
|
||||
const { logger, org, env, db } = ctx;
|
||||
|
||||
const { subId, anchorToUnix, config, ...rest } =
|
||||
metadata.data as AttachParams;
|
||||
|
||||
const attachParams = rest;
|
||||
|
||||
if (!attachParams) return;
|
||||
|
||||
const reqMatch =
|
||||
attachParams.org.id === org.id && attachParams.customer.env === env;
|
||||
|
||||
if (!reqMatch) return;
|
||||
|
||||
const batchInsert = [];
|
||||
for (const product of attachParams.products) {
|
||||
batchInsert.push(
|
||||
createFullCusProduct({
|
||||
db,
|
||||
attachParams: attachToInsertParams(attachParams, product),
|
||||
subscriptionIds: subId ? [subId] : undefined,
|
||||
anchorToUnix,
|
||||
carryExistingUsages: config?.carryUsage,
|
||||
scenario: AttachScenario.New,
|
||||
logger: logger,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batchInsert);
|
||||
|
||||
logger.info(
|
||||
`✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`,
|
||||
);
|
||||
|
||||
await deleteCachedApiCustomer({
|
||||
customerId: attachParams.customer.id || "",
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
};
|
||||
45
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoicePaidMetadata.ts
vendored
Normal file
45
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoicePaidMetadata.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import { MetadataType } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
|
||||
import { MetadataService } from "../../../../internal/metadata/MetadataService";
|
||||
import { handleInvoiceActionRequiredCompleted } from "./handleInvoiceActionRequiredCompleted";
|
||||
import { handleInvoiceCheckoutPaid } from "./handleInvoiceCheckoutPaid";
|
||||
|
||||
export const handleInvoicePaidMetadata = async ({
|
||||
ctx,
|
||||
invoice,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
invoice: Stripe.Invoice;
|
||||
}) => {
|
||||
const metadataId = invoice.metadata?.autumn_metadata_id;
|
||||
|
||||
if (!metadataId) return;
|
||||
|
||||
const metadata = await MetadataService.get({
|
||||
db: ctx.db,
|
||||
id: metadataId,
|
||||
});
|
||||
|
||||
if (!metadata) return;
|
||||
|
||||
if (metadata.type === MetadataType.InvoiceActionRequired) {
|
||||
await handleInvoiceActionRequiredCompleted({
|
||||
ctx,
|
||||
invoice,
|
||||
metadata,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await handleInvoiceCheckoutPaid({
|
||||
ctx,
|
||||
metadata,
|
||||
});
|
||||
|
||||
await MetadataService.delete({
|
||||
db: ctx.db,
|
||||
id: metadata.id,
|
||||
});
|
||||
};
|
||||
@@ -38,11 +38,13 @@ const handleInvoiceCheckoutVoided = async ({
|
||||
id: metadataId,
|
||||
});
|
||||
|
||||
if (!metadata) return;
|
||||
|
||||
const {
|
||||
anchorToUnix: _anchorToUnix,
|
||||
config: _config,
|
||||
...rest
|
||||
} = metadata?.data || {};
|
||||
} = metadata.data as AttachParams;
|
||||
|
||||
const attachParams = rest as AttachParams;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { MetadataInsert, MetadataType } from "@autumn/shared";
|
||||
import { addDays } from "date-fns";
|
||||
import type { DrizzleCli } from "../../../../db/initDrizzle";
|
||||
import { generateId } from "../../../../utils/genUtils";
|
||||
import type { AttachParams } from "../../../customers/cusProducts/AttachParams";
|
||||
import { MetadataService } from "../../../metadata/MetadataService";
|
||||
|
||||
export const attachParamsToMetadata = async ({
|
||||
db,
|
||||
attachParams,
|
||||
type,
|
||||
stripeInvoiceId,
|
||||
expiresAt,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
attachParams: AttachParams;
|
||||
type: MetadataType;
|
||||
stripeInvoiceId?: string;
|
||||
expiresAt?: number;
|
||||
}) => {
|
||||
const {
|
||||
req: _req,
|
||||
checkoutSessionParams: _checkoutSessionParams,
|
||||
stripeCli: _stripeCli,
|
||||
paymentMethod: _paymentMethod,
|
||||
...rest
|
||||
} = attachParams;
|
||||
|
||||
const attachClone = structuredClone(rest);
|
||||
|
||||
const metadata: MetadataInsert = {
|
||||
id: generateId("meta"),
|
||||
created_at: Date.now(),
|
||||
expires_at: expiresAt ?? addDays(Date.now(), 10).getTime(),
|
||||
data: attachClone,
|
||||
type,
|
||||
stripe_invoice_id: stripeInvoiceId,
|
||||
};
|
||||
|
||||
await MetadataService.insert({ db, data: metadata });
|
||||
|
||||
return metadata;
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
type AttachConfig,
|
||||
AttachFunctionResponseSchema,
|
||||
MetadataType,
|
||||
RecaseError,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
|
||||
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
|
||||
import { orgToCurrency } from "@/internal/orgs/orgUtils.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
@@ -15,6 +15,7 @@ import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingInter
|
||||
import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
|
||||
import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js";
|
||||
import type { AttachParams } from "../cusProducts/AttachParams.js";
|
||||
|
||||
export const handleCreateCheckout = async ({
|
||||
@@ -57,9 +58,10 @@ export const handleCreateCheckout = async ({
|
||||
const isRecurring = pricesContainRecurring(attachParams.prices);
|
||||
|
||||
// Insert metadata
|
||||
const metaId = await createCheckoutMetadata({
|
||||
const metadata = await attachParamsToMetadata({
|
||||
db,
|
||||
attachParams,
|
||||
type: MetadataType.CheckoutSessionCompleted,
|
||||
});
|
||||
|
||||
let billingCycleAnchorUnixSeconds = org.config.anchor_start_of_month
|
||||
@@ -136,7 +138,7 @@ export const handleCreateCheckout = async ({
|
||||
metadata: {
|
||||
...(attachParams.metadata ? attachParams.metadata : {}),
|
||||
...(checkoutParams?.metadata || {}),
|
||||
autumn_metadata_id: metaId,
|
||||
autumn_metadata_id: metadata.id,
|
||||
},
|
||||
payment_method_collection:
|
||||
freeTrial &&
|
||||
@@ -155,7 +157,7 @@ export const handleCreateCheckout = async ({
|
||||
...checkoutParams,
|
||||
metadata: {
|
||||
...(checkoutParams?.metadata || {}),
|
||||
autumn_metadata_id: metaId,
|
||||
autumn_metadata_id: metadata.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ import {
|
||||
type AttachConfig,
|
||||
type AttachFunctionResponse,
|
||||
AttachFunctionResponseSchema,
|
||||
MetadataType,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
|
||||
import { isOneOff } from "@/internal/products/productUtils.js";
|
||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
|
||||
import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js";
|
||||
import { handleOneOffFunction } from "../attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
|
||||
import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js";
|
||||
import type { AttachParams } from "../cusProducts/AttachParams.js";
|
||||
@@ -39,13 +40,9 @@ export const handleCreateInvoiceCheckout = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// const { invoices, anchorToUnix, subs } = invoiceResult;
|
||||
const { invoice, stripeSub, anchorToUnix } = invoiceResult;
|
||||
|
||||
// console.log("finalize invoice:", config.finalizeInvoice);
|
||||
// console.log("invoice hosted url:", invoice?.hosted_invoice_url);
|
||||
|
||||
const metadataId = await createCheckoutMetadata({
|
||||
const metadata = await attachParamsToMetadata({
|
||||
db: ctx.db,
|
||||
attachParams: {
|
||||
...attachParams,
|
||||
@@ -53,12 +50,13 @@ export const handleCreateInvoiceCheckout = async ({
|
||||
subId: stripeSub?.id,
|
||||
config,
|
||||
},
|
||||
type: MetadataType.InvoiceCheckout,
|
||||
});
|
||||
|
||||
if (invoice) {
|
||||
await stripeCli.invoices.update(invoice.id, {
|
||||
metadata: {
|
||||
autumn_metadata_id: metadataId,
|
||||
autumn_metadata_id: metadata.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -73,52 +71,5 @@ export const handleCreateInvoiceCheckout = async ({
|
||||
message: `Successfully created invoice checkout for customer ${customerId}, product(s) ${productNames}`,
|
||||
code: SuccessCode.CheckoutCreated,
|
||||
invoice: config.finalizeInvoice ? undefined : invoice, // if finalizeInvoice, checkout_url is used
|
||||
// invoice,
|
||||
// stripeSub,
|
||||
// anchorToUnix,
|
||||
// config,
|
||||
});
|
||||
|
||||
// if (res) {
|
||||
// if (!config.finalizeInvoice) {
|
||||
// res.status(200).json(
|
||||
// AttachResultSchema.parse({
|
||||
// invoice: invoices[0],
|
||||
// code: SuccessCode.CheckoutCreated,
|
||||
// message: `Successfully created invoice for customer ${
|
||||
// attachParams.customer.id || attachParams.customer.internal_id
|
||||
// }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
|
||||
// product_ids: attachParams.products.map((p) => p.id),
|
||||
// customer_id:
|
||||
// attachParams.customer.id || attachParams.customer.internal_id,
|
||||
// }),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// res.status(200).json(
|
||||
// AttachResultSchema.parse({
|
||||
// checkout_url: invoices[0].hosted_invoice_url,
|
||||
// code: SuccessCode.CheckoutCreated,
|
||||
// message: `Successfully created invoice checkout for customer ${
|
||||
// attachParams.customer.id || attachParams.customer.internal_id
|
||||
// }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
|
||||
// product_ids: attachParams.products.map((p) => p.id),
|
||||
// customer_id:
|
||||
// attachParams.customer.id || attachParams.customer.internal_id,
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return { invoices };
|
||||
};
|
||||
|
||||
// if (attachParams.productsList) {
|
||||
// invoiceResult = await handleMultiAttachFlow({
|
||||
// req,
|
||||
// res,
|
||||
// attachParams,
|
||||
// attachBody,
|
||||
// branch,
|
||||
// config,
|
||||
// });
|
||||
// } else
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { type AppEnv, AttachScenario, type Organization } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { MetadataService } from "@/internal/metadata/MetadataService.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { deleteCachedApiCustomer } from "../../../cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
|
||||
|
||||
export const handleInvoiceCheckoutPaid = async ({
|
||||
req,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
stripeCli,
|
||||
invoice,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
db: DrizzleCli;
|
||||
stripeCli: Stripe;
|
||||
invoice: Stripe.Invoice;
|
||||
}) => {
|
||||
const { logger } = req;
|
||||
const metadataId = invoice.metadata?.autumn_metadata_id;
|
||||
|
||||
if (!metadataId) return;
|
||||
|
||||
const metadata = await MetadataService.get({
|
||||
db,
|
||||
id: metadataId,
|
||||
});
|
||||
|
||||
const { subId, anchorToUnix, config, ...rest }: AttachParams =
|
||||
metadata?.data ?? {};
|
||||
|
||||
const attachParams = rest;
|
||||
|
||||
if (!attachParams) return;
|
||||
|
||||
const reqMatch =
|
||||
attachParams.org.id === org.id && attachParams.customer.env === env;
|
||||
|
||||
if (!reqMatch) return;
|
||||
|
||||
// if (attachParams.productsList) {
|
||||
// console.log("Inserting products list");
|
||||
// for (const productOptions of attachParams.productsList) {
|
||||
// const product = attachParams.products.find(
|
||||
// (p) => p.id === productOptions.product_id,
|
||||
// );
|
||||
|
||||
// if (!product) {
|
||||
// logger.error(
|
||||
// `checkout.completed: product not found for productOptions: ${JSON.stringify(
|
||||
// productOptions,
|
||||
// )}`,
|
||||
// );
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// await createFullCusProduct({
|
||||
// db,
|
||||
// attachParams: attachToInsertParams(
|
||||
// attachParams,
|
||||
// product,
|
||||
// productOptions.entity_id || undefined,
|
||||
// ),
|
||||
// subscriptionIds: subIds,
|
||||
// anchorToUnix,
|
||||
// scenario: AttachScenario.New,
|
||||
// logger,
|
||||
// productOptions,
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
|
||||
// }
|
||||
|
||||
const batchInsert = [];
|
||||
for (const product of attachParams.products) {
|
||||
batchInsert.push(
|
||||
createFullCusProduct({
|
||||
db,
|
||||
attachParams: attachToInsertParams(attachParams, product),
|
||||
subscriptionIds: subId ? [subId] : undefined,
|
||||
anchorToUnix,
|
||||
carryExistingUsages: config?.carryUsage,
|
||||
scenario: AttachScenario.New,
|
||||
logger: req.logger,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batchInsert);
|
||||
|
||||
req.logger.info(
|
||||
`✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`,
|
||||
);
|
||||
|
||||
await deleteCachedApiCustomer({
|
||||
customerId: attachParams.customer.id || "",
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
};
|
||||
@@ -14,7 +14,7 @@ import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSu
|
||||
import { subIsCanceled } 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";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
import {
|
||||
@@ -123,11 +123,6 @@ export const handleUpgradeFlow = async ({
|
||||
logger.info(`UPGRADE FLOW, updating sub ${curSub.id}`);
|
||||
itemSet.subItems = subItems;
|
||||
|
||||
// await logPhaseItems({
|
||||
// db: req.db,
|
||||
// items: itemSet.subItems,
|
||||
// });
|
||||
|
||||
const res = await updateStripeSub2({
|
||||
ctx,
|
||||
attachParams,
|
||||
@@ -147,6 +142,14 @@ export const handleUpgradeFlow = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (res?.url) {
|
||||
return AttachFunctionResponseSchema.parse({
|
||||
checkout_url: res.url,
|
||||
code: SuccessCode.InvoiceActionRequired,
|
||||
message: `Payment action required`,
|
||||
});
|
||||
}
|
||||
|
||||
const schedule = await paramsToCurSubSchedule({ attachParams });
|
||||
|
||||
if (schedule) {
|
||||
|
||||
@@ -81,6 +81,7 @@ export const updateStripeSub2 = async ({
|
||||
days_until_due: 30,
|
||||
}),
|
||||
payment_behavior: "error_if_incomplete",
|
||||
|
||||
expand: ["latest_invoice"],
|
||||
});
|
||||
|
||||
@@ -120,8 +121,10 @@ export const updateStripeSub2 = async ({
|
||||
logger,
|
||||
});
|
||||
|
||||
let url = null;
|
||||
if (proration === ProrationBehavior.Immediately) {
|
||||
latestInvoice = await createProrationInvoice({
|
||||
const res = await createProrationInvoice({
|
||||
ctx,
|
||||
attachParams,
|
||||
invoiceOnly,
|
||||
curSub,
|
||||
@@ -129,20 +132,30 @@ export const updateStripeSub2 = async ({
|
||||
logger,
|
||||
});
|
||||
|
||||
latestInvoice = res.invoice;
|
||||
url = res.url;
|
||||
|
||||
console.log(`FINALIZED INVOICE ${latestInvoice?.id}`);
|
||||
console.log(latestInvoice?.lines.data.map((line) => line.description));
|
||||
}
|
||||
|
||||
// If url is returned, it means invoice action is required, so don't reset balances.
|
||||
if (!url) {
|
||||
await resetUsageBalances({
|
||||
db,
|
||||
cusEntIds,
|
||||
cusProduct: curMainProduct!,
|
||||
});
|
||||
} else {
|
||||
// reset balances later when invoice is paid
|
||||
attachParams.cusEntIds = cusEntIds;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedSub,
|
||||
latestInvoice: latestInvoice,
|
||||
cusEntIds,
|
||||
replaceables,
|
||||
url,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -120,10 +120,10 @@ export const paramsToSubItems = async ({
|
||||
? removeCusProducts!
|
||||
: getCusProductsToRemove({ attachParams });
|
||||
|
||||
console.log(
|
||||
"Cus products to remove:",
|
||||
cusProductsToRemove.map((cp) => cp.product.name),
|
||||
);
|
||||
// console.log(
|
||||
// "Cus products to remove:",
|
||||
// cusProductsToRemove.map((cp) => cp.product.name),
|
||||
// );
|
||||
|
||||
const newSubItems = mergeNewSubItems({
|
||||
itemSet,
|
||||
|
||||
@@ -82,6 +82,10 @@ export type AttachParams = {
|
||||
anchorToUnix?: number;
|
||||
subId?: string;
|
||||
config?: AttachConfig;
|
||||
|
||||
// Invoice action required
|
||||
stripeInvoiceId?: string;
|
||||
cusEntIds?: string[];
|
||||
};
|
||||
|
||||
export type InsertCusProductParams = {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnMetadata, metadata } from "@autumn/shared";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
type Metadata,
|
||||
type MetadataInsert,
|
||||
type MetadataType,
|
||||
metadata,
|
||||
} from "@autumn/shared";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
|
||||
export class MetadataService {
|
||||
static async insert({ db, data }: { db: DrizzleCli; data: AutumnMetadata }) {
|
||||
static async insert({ db, data }: { db: DrizzleCli; data: MetadataInsert }) {
|
||||
await db.insert(metadata).values(data);
|
||||
}
|
||||
|
||||
@@ -18,6 +23,33 @@ export class MetadataService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data[0] as AutumnMetadata;
|
||||
return data[0] as Metadata;
|
||||
}
|
||||
|
||||
static async getByStripeInvoiceId({
|
||||
db,
|
||||
stripeInvoiceId,
|
||||
type,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
stripeInvoiceId: string;
|
||||
type?: MetadataType;
|
||||
}) {
|
||||
const meta = await db.query.metadata.findFirst({
|
||||
where: and(
|
||||
eq(metadata.stripe_invoice_id, stripeInvoiceId),
|
||||
type ? eq(metadata.type, type) : undefined,
|
||||
),
|
||||
});
|
||||
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return meta as Metadata;
|
||||
}
|
||||
|
||||
static async delete({ db, id }: { db: DrizzleCli; id: string }) {
|
||||
await db.delete(metadata).where(eq(metadata.id, id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,7 @@
|
||||
import type { AutumnMetadata } from "@autumn/shared";
|
||||
import { addDays } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import type { AttachParams } from "../customers/cusProducts/AttachParams.js";
|
||||
import { MetadataService } from "./MetadataService.js";
|
||||
|
||||
export const createCheckoutMetadata = async ({
|
||||
db,
|
||||
attachParams,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
attachParams: AttachParams;
|
||||
}) => {
|
||||
const metaId = generateId("meta");
|
||||
|
||||
const {
|
||||
req: _req,
|
||||
checkoutSessionParams: _checkoutSessionParams,
|
||||
stripeCli: _stripeCli,
|
||||
paymentMethod: _paymentMethod,
|
||||
...rest
|
||||
} = attachParams;
|
||||
|
||||
const attachClone = structuredClone(rest);
|
||||
|
||||
const metadata: AutumnMetadata = {
|
||||
id: metaId,
|
||||
created_at: Date.now(),
|
||||
expires_at: addDays(Date.now(), 10).getTime(), // 10 days
|
||||
data: {
|
||||
...attachClone,
|
||||
},
|
||||
};
|
||||
|
||||
await MetadataService.insert({ db, data: metadata });
|
||||
|
||||
return metaId;
|
||||
};
|
||||
|
||||
export const getMetadataFromCheckoutSession = async (
|
||||
checkoutSession: Stripe.Checkout.Session,
|
||||
db: DrizzleCli,
|
||||
|
||||
@@ -150,10 +150,27 @@ export const attachPaymentMethod = async ({
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
stripeCusId: string;
|
||||
type: "success" | "fail";
|
||||
type: "success" | "fail" | "authenticate";
|
||||
}) => {
|
||||
try {
|
||||
const token = type === "fail" ? "tok_chargeCustomerFail" : "tok_visa";
|
||||
|
||||
if (type === "authenticate") {
|
||||
await stripeCli.paymentMethods.attach("pm_card_authenticationRequired", {
|
||||
customer: stripeCusId,
|
||||
});
|
||||
|
||||
const pms = await stripeCli.paymentMethods.list({
|
||||
customer: stripeCusId,
|
||||
});
|
||||
|
||||
await stripeCli.customers.update(stripeCusId, {
|
||||
invoice_settings: {
|
||||
default_payment_method: pms.data[0].id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const pm = await stripeCli.paymentMethods.create({
|
||||
type: "card",
|
||||
card: {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const initCustomerV3 = async ({
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
customerId: string;
|
||||
attachPm?: "success" | "fail";
|
||||
attachPm?: "success" | "fail" | "authenticate";
|
||||
customerData?: CustomerData;
|
||||
withTestClock?: boolean;
|
||||
withDefault?: boolean;
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { LegacyVersion } from "@autumn/shared";
|
||||
import { ApiVersion } 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 { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import {
|
||||
constructProduct,
|
||||
constructRawProduct,
|
||||
} from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructPriceItem } from "../../src/internal/products/product-items/productItemUtils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { attachAuthenticatePaymentMethod } from "../../src/external/stripe/stripeCusUtils.js";
|
||||
import { initCustomerV3 } from "../../src/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "../../src/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { expectProductAttached } from "../utils/expectUtils/expectProductAttached.js";
|
||||
import { expectSubItemsCorrect } from "../utils/expectUtils/expectSubUtils.js";
|
||||
import { completeInvoiceConfirmation } from "../utils/stripeUtils/completeInvoiceConfirmation.js";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: true,
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
@@ -27,13 +27,9 @@ const pro = constructProduct({
|
||||
],
|
||||
});
|
||||
|
||||
const oneOff = constructRawProduct({
|
||||
id: "one-off",
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
items: [
|
||||
constructPriceItem({
|
||||
price: 10,
|
||||
interval: null,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
@@ -43,7 +39,7 @@ const oneOff = constructRawProduct({
|
||||
|
||||
describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
||||
const customerId = "temp";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
@@ -56,40 +52,61 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro, oneOff],
|
||||
products: [pro, premium],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro product", async () => {
|
||||
// await autumn.customers.get(customerId);
|
||||
|
||||
const res = await autumn.attach({
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: oneOff.id,
|
||||
await attachAuthenticatePaymentMethod({
|
||||
ctx,
|
||||
customerId,
|
||||
});
|
||||
await autumn.attach({
|
||||
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: oneOff.id,
|
||||
product_id: premium.id,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
console.log("Customer:", customer);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: oneOff.id,
|
||||
// });
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: oneOff.id,
|
||||
// });
|
||||
await expectSubItemsCorrect({
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli: ctx.stripeCli,
|
||||
db: ctx.db,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
});
|
||||
|
||||
// const customer = await autumn.customers.get(customerId);
|
||||
// console.log("Customer:", customer);
|
||||
await completeInvoiceConfirmation({
|
||||
url: res.checkout_url,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have premium product attached", async () => {
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: premium,
|
||||
});
|
||||
|
||||
await expectSubItemsCorrect({
|
||||
customerId,
|
||||
product: premium,
|
||||
stripeCli: ctx.stripeCli,
|
||||
db: ctx.db,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { expectSubItemsCorrect } from "@tests/utils/expectUtils/expectSubUtils.js";
|
||||
import { completeInvoiceConfirmation } from "@tests/utils/stripeUtils/completeInvoiceConfirmation.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { attachAuthenticatePaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
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";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Credits,
|
||||
includedUsage: 200,
|
||||
// unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("invoice-action-required1: Testing invoice action required")}`, () => {
|
||||
const customerId = "invoice-action-required1";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro, premium],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
let checkoutUrl: string;
|
||||
test("should attach pro product, then upgrade to premium and get checkout_url", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await attachAuthenticatePaymentMethod({
|
||||
ctx,
|
||||
customerId,
|
||||
});
|
||||
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
|
||||
expect(res.checkout_url).toBeDefined();
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
await expectSubItemsCorrect({
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli: ctx.stripeCli,
|
||||
db: ctx.db,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
});
|
||||
|
||||
checkoutUrl = res.checkout_url;
|
||||
});
|
||||
|
||||
test("should complete invoice action required and have premium product attached", async () => {
|
||||
await completeInvoiceConfirmation({
|
||||
url: checkoutUrl,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: premium,
|
||||
});
|
||||
|
||||
await expectSubItemsCorrect({
|
||||
customerId,
|
||||
product: premium,
|
||||
stripeCli: ctx.stripeCli,
|
||||
db: ctx.db,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
});
|
||||
|
||||
// Cleared cache
|
||||
const nonCachedCustomer = await autumn.customers.get(customerId, {
|
||||
skip_cache: "true",
|
||||
});
|
||||
expect(nonCachedCustomer.invoices?.[0].status).toBe("paid");
|
||||
|
||||
expectProductAttached({
|
||||
customer: nonCachedCustomer,
|
||||
product: premium,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion } 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 { attachAuthenticatePaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
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 { handleVoidInvoiceCron } from "../../../src/cron/invoiceCron/runInvoiceCron";
|
||||
import { MetadataService } from "../../../src/internal/metadata/MetadataService";
|
||||
import { timeout } from "../../utils/genUtils";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Credits,
|
||||
includedUsage: 200,
|
||||
// unlimited: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("invoice-action-required2: Testing void invoice cron")}`, () => {
|
||||
const customerId = "invoice-action-required2";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro, premium],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro product, then upgrade to premium and get checkout_url", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await attachAuthenticatePaymentMethod({
|
||||
ctx,
|
||||
customerId,
|
||||
});
|
||||
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
|
||||
// Get latest invoice for this customer
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expect(customer.invoices?.[0].status).toBe("open");
|
||||
|
||||
const stripeInvoices = await ctx.stripeCli.invoices.list({
|
||||
customer: customer.stripe_id!,
|
||||
});
|
||||
|
||||
const latestInvoice = stripeInvoices.data[0];
|
||||
|
||||
expect(latestInvoice.metadata?.autumn_metadata_id).toBeDefined();
|
||||
const metadata = await MetadataService.get({
|
||||
db: ctx.db,
|
||||
id: latestInvoice.metadata?.autumn_metadata_id ?? "",
|
||||
});
|
||||
|
||||
await handleVoidInvoiceCron({
|
||||
metadata: metadata!,
|
||||
ctx: {
|
||||
db: ctx.db,
|
||||
logger: ctx.logger,
|
||||
},
|
||||
});
|
||||
|
||||
const voidedInvoice = await ctx.stripeCli.invoices.retrieve(
|
||||
latestInvoice.id,
|
||||
);
|
||||
expect(voidedInvoice.status).toBe("void");
|
||||
|
||||
await timeout(3000);
|
||||
const customer2 = await autumn.customers.get(customerId);
|
||||
expect(customer2.invoices?.[0].status).toBe("void");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
|
||||
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js";
|
||||
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
|
||||
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { expectSubItemsCorrect } from "@tests/utils/expectUtils/expectSubUtils.js";
|
||||
@@ -22,7 +21,9 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
const testCase = "upgrade6";
|
||||
import { completeInvoiceCheckout } from "../../utils/stripeUtils/completeInvoiceCheckout";
|
||||
|
||||
const testCase = "invoice-action-required3";
|
||||
|
||||
export const pro = constructProduct({
|
||||
items: [
|
||||
@@ -46,7 +47,7 @@ export const premium = constructProduct({
|
||||
type: "premium",
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => {
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing upgrade, failed payment`)}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||
let testClockId: string;
|
||||
@@ -89,6 +90,8 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () =>
|
||||
});
|
||||
|
||||
const usage = 100012;
|
||||
|
||||
let checkoutUrl: string;
|
||||
test("should upgrade to premium product and fail", async () => {
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
@@ -106,22 +109,14 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () =>
|
||||
await attachFailedPaymentMethod({ stripeCli, customer: cus! });
|
||||
await timeout(2000);
|
||||
|
||||
await expectAutumnError({
|
||||
func: async () => {
|
||||
await attachAndExpectCorrect({
|
||||
autumn,
|
||||
customerId,
|
||||
product: premium,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
},
|
||||
errMessage: "Failed to update subscription. Your card was declined.",
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
|
||||
await timeout(4000);
|
||||
checkoutUrl = res.checkout_url;
|
||||
expect(res.checkout_url).toBeDefined();
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
@@ -149,4 +144,30 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () =>
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
test("should complete invoice and have premium product attached", async () => {
|
||||
await completeInvoiceCheckout({
|
||||
url: checkoutUrl,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: premium,
|
||||
});
|
||||
|
||||
expectFeaturesCorrect({
|
||||
customer,
|
||||
product: premium,
|
||||
});
|
||||
|
||||
await expectSubItemsCorrect({
|
||||
customerId,
|
||||
product: premium,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,7 +122,7 @@ export const attachAndExpectCorrect = async ({
|
||||
await timeout(waitForInvoice);
|
||||
}
|
||||
|
||||
let customer;
|
||||
let customer: Customer;
|
||||
if (entityId) {
|
||||
customer = await autumn.entities.get(customerId, entityId);
|
||||
} else {
|
||||
|
||||
128
server/tests/utils/stripeUtils/completeInvoiceConfirmation.ts
Normal file
128
server/tests/utils/stripeUtils/completeInvoiceConfirmation.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import puppeteer, { type Browser } from "puppeteer-core";
|
||||
import { timeout } from "../genUtils.js";
|
||||
|
||||
// const client = new Hyperbrowser({
|
||||
// apiKey: process.env.HYPERBROWSER_API_KEY,
|
||||
// });
|
||||
|
||||
export const completeInvoiceConfirmation = async ({
|
||||
url,
|
||||
isLocal = false,
|
||||
}: {
|
||||
url: string;
|
||||
isLocal?: boolean;
|
||||
}) => {
|
||||
let browser: Browser;
|
||||
|
||||
// if (process.env.NODE_ENV === "development" && !isLocal) {
|
||||
// const session = await client.sessions.create();
|
||||
// browser = await puppeteer.connect({
|
||||
// browserWSEndpoint: session!.wsEndpoint,
|
||||
// defaultViewport: null,
|
||||
// });
|
||||
// } else {
|
||||
|
||||
// }
|
||||
browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
|
||||
await page.goto(url);
|
||||
|
||||
// Wait for the page to be ready
|
||||
await page.waitForSelector("button", { timeout: 5000 });
|
||||
|
||||
// Find and click the "Confirm payment" button
|
||||
const buttonClicked = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll("button"));
|
||||
const confirmBtn = buttons.find((b) =>
|
||||
/confirm payment/i.test(b.textContent || ""),
|
||||
);
|
||||
if (confirmBtn) {
|
||||
(confirmBtn as HTMLElement).click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!buttonClicked) {
|
||||
throw new Error("Could not find or click Confirm payment button");
|
||||
}
|
||||
|
||||
// Wait for processing/navigation
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Wait for iframe with the three-ds-2-challenge URL
|
||||
let threeDSFrame = null;
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
const frames = page.frames();
|
||||
|
||||
threeDSFrame = frames.find((f) =>
|
||||
f.url().includes("three-ds-2-challenge"),
|
||||
);
|
||||
|
||||
if (threeDSFrame) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!threeDSFrame) {
|
||||
throw new Error("Could not find 3DS challenge frame");
|
||||
}
|
||||
|
||||
// Wait for the 3DS frame content to load
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Check for nested iframes
|
||||
const frameContent = await threeDSFrame.evaluate(() => {
|
||||
return {
|
||||
hasButton: !!document.querySelector("#test-source-authorize-3ds"),
|
||||
iframes: document.querySelectorAll("iframe").length,
|
||||
};
|
||||
});
|
||||
|
||||
// If there's a nested iframe, find it
|
||||
if (frameContent.iframes > 0) {
|
||||
const childFrames = page.frames();
|
||||
let challengeFrame = childFrames.find((f) =>
|
||||
f.url().includes("3d_secure_2_test"),
|
||||
);
|
||||
|
||||
if (!challengeFrame) {
|
||||
challengeFrame = childFrames.find((f) => f.name() === "challengeFrame");
|
||||
}
|
||||
|
||||
if (challengeFrame) {
|
||||
threeDSFrame = challengeFrame;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the button and click it
|
||||
await threeDSFrame.waitForSelector("#test-source-authorize-3ds", {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
await threeDSFrame.evaluate(() => {
|
||||
const button = document.querySelector(
|
||||
"#test-source-authorize-3ds",
|
||||
) as HTMLElement;
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the 3DS authentication to complete
|
||||
await timeout(10000);
|
||||
} finally {
|
||||
// always close browser
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,10 @@ import { type DrizzleCli, initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { logger } from "../../../src/external/logtail/logtailUtils.js";
|
||||
import {
|
||||
type Logger,
|
||||
logger,
|
||||
} from "../../../src/external/logtail/logtailUtils.js";
|
||||
|
||||
const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
@@ -19,6 +22,7 @@ export type TestContext = {
|
||||
db: DrizzleCli;
|
||||
orgSecretKey: string;
|
||||
features: Feature[];
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
export const createTestContext = async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum SuccessCode {
|
||||
InvoiceActionRequired = "invoice_action_required",
|
||||
// Track
|
||||
SuccessfullyDeducted = "successfully_deducted",
|
||||
EventReceived = "event_received",
|
||||
|
||||
@@ -104,7 +104,6 @@ export * from "./models/orgModels/frontendOrg.js";
|
||||
export * from "./models/orgModels/orgConfig.js";
|
||||
export * from "./models/orgModels/orgConfig.js";
|
||||
export * from "./models/orgModels/orgTable.js";
|
||||
export * from "./models/otherModels/metadataModels.js";
|
||||
export * from "./models/otherModels/metadataTable.js";
|
||||
// Duration Types
|
||||
export * from "./models/productModels/durationTypes/rolloverExpiryDurationType.js";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { z } from "zod/v4";
|
||||
// import { z } from "zod/v4";
|
||||
|
||||
export const AutumnMetadataSchema = z.object({
|
||||
id: z.string(),
|
||||
created_at: z.number(),
|
||||
expires_at: z.number(),
|
||||
data: z.any(),
|
||||
});
|
||||
// export const AutumnMetadataSchema = z.object({
|
||||
// id: z.string(),
|
||||
// created_at: z.number(),
|
||||
// expires_at: z.number(),
|
||||
// data: z.any(),
|
||||
// });
|
||||
|
||||
export type AutumnMetadata = z.infer<typeof AutumnMetadataSchema>;
|
||||
// export type AutumnMetadata = z.infer<typeof AutumnMetadataSchema>;
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { pgTable, text, numeric, jsonb } from "drizzle-orm/pg-core";
|
||||
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
|
||||
import { jsonb, numeric, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { sqlNow } from "../../db/utils.js";
|
||||
|
||||
export enum MetadataType {
|
||||
InvoiceActionRequired = "invoice_action_required",
|
||||
InvoiceCheckout = "invoice_checkout",
|
||||
CheckoutSessionCompleted = "checkout_session_completed",
|
||||
}
|
||||
|
||||
export const metadata = pgTable("metadata", {
|
||||
id: text().primaryKey().notNull(),
|
||||
created_at: numeric({ mode: "number" }).notNull().default(sqlNow),
|
||||
expires_at: numeric({ mode: "number" }),
|
||||
data: jsonb(),
|
||||
type: text("type").$type<MetadataType>(),
|
||||
stripe_invoice_id: text("stripe_invoice_id"),
|
||||
});
|
||||
|
||||
export type Metadata = InferSelectModel<typeof metadata>;
|
||||
export type MetadataInsert = InferInsertModel<typeof metadata>;
|
||||
|
||||
Reference in New Issue
Block a user