fix: when payment method is alipay, just remove
This commit is contained in:
1
server/src/external/stripe/stripeCusUtils.ts
vendored
1
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -111,6 +111,7 @@ export const getCusPaymentMethod = async ({
|
||||
const paymentMethod = await stripeCli.paymentMethods.retrieve(
|
||||
paymentMethodId as string,
|
||||
);
|
||||
|
||||
return paymentMethod;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export const payForInvoice = async ({
|
||||
statusCode: 400,
|
||||
});
|
||||
} else {
|
||||
const invoice = await stripeCli.invoices.retrieve(invoiceId);
|
||||
return {
|
||||
paid: false,
|
||||
error: new RecaseError({
|
||||
@@ -71,7 +72,7 @@ export const payForInvoice = async ({
|
||||
code: ErrCode.CustomerHasNoPaymentMethod,
|
||||
statusCode: 400,
|
||||
}),
|
||||
invoice: null,
|
||||
invoice: invoice,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ export const createStripeSub2 = async ({
|
||||
paymentMethodData = {
|
||||
default_payment_method: paymentMethod.id,
|
||||
};
|
||||
// if (paymentMethod.type === "alipay") {
|
||||
|
||||
// } else {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
const { subItems, invoiceItems, usageFeatures } = itemSet;
|
||||
@@ -70,6 +75,7 @@ export const createStripeSub2 = async ({
|
||||
payment_behavior: isCustomPaymentMethod
|
||||
? "default_incomplete"
|
||||
: "allow_incomplete",
|
||||
// payment_behavior: "default_incomplete",
|
||||
|
||||
add_invoice_items: invoiceItems,
|
||||
collection_method: invoiceOnly ? "send_invoice" : "charge_automatically",
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type AttachConfig,
|
||||
type AttachReplaceable,
|
||||
ProrationBehavior,
|
||||
RecaseError,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||
@@ -11,7 +10,6 @@ import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubI
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { SubService } from "@/internal/subscriptions/SubService.js";
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import type { ItemSet } from "@/utils/models/ItemSet.js";
|
||||
import { createProrationInvoice } from "../../../../../external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.js";
|
||||
import { isStripeSubscriptionCanceling } from "../../../../../external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils.js";
|
||||
@@ -44,12 +42,12 @@ export const updateStripeSub2 = async ({
|
||||
const { stripeCli, paymentMethod } = attachParams;
|
||||
const { invoiceOnly, proration } = config;
|
||||
|
||||
if (config.requirePaymentMethod !== false && nullish(paymentMethod)) {
|
||||
throw new RecaseError({
|
||||
message: "Payment method is required",
|
||||
code: "payment_method_required",
|
||||
});
|
||||
}
|
||||
// if (config.requirePaymentMethod !== false && nullish(paymentMethod)) {
|
||||
// throw new RecaseError({
|
||||
// message: "Payment method is required",
|
||||
// code: "payment_method_required",
|
||||
// });
|
||||
// }
|
||||
|
||||
if (curSub.billing_mode.type !== "flexible") {
|
||||
curSub = await stripeCli.subscriptions.migrate(curSub.id, {
|
||||
|
||||
@@ -41,12 +41,23 @@ export const getStripeCusData = async ({
|
||||
: undefined;
|
||||
|
||||
if (!paymentMethod) {
|
||||
// paymentMethod =
|
||||
// (await getCusPaymentMethod({
|
||||
// stripeCli,
|
||||
// stripeId: stripeCus.id,
|
||||
// })) ?? undefined;
|
||||
|
||||
const paymentMethods = await listCusPaymentMethods({
|
||||
stripeCli,
|
||||
stripeId: stripeCus.id,
|
||||
});
|
||||
|
||||
paymentMethod = paymentMethods.length ? paymentMethods[0] : undefined;
|
||||
|
||||
if (paymentMethod?.type === "alipay") {
|
||||
await stripeCli.paymentMethods.detach(paymentMethod.id);
|
||||
paymentMethod = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return { stripeCus, paymentMethod, now };
|
||||
|
||||
@@ -43,11 +43,6 @@ const handleNonCheckoutErrors = ({
|
||||
message: `Not allowed to ${action} when using force_checkout`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
} else if (noPaymentMethod) {
|
||||
throw new RecaseError({
|
||||
message: `Not allowed to ${action} because customer has no payment method on file`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
} else if (config.invoiceCheckout) {
|
||||
throw new RecaseError({
|
||||
message: `Not allowed to ${action} when using 'invoice': true`,
|
||||
|
||||
@@ -148,7 +148,7 @@ export const attachPaymentMethod = async ({
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
stripeCusId: string;
|
||||
type: "success" | "fail" | "authenticate";
|
||||
type: "success" | "fail" | "authenticate" | "alipay";
|
||||
}) => {
|
||||
try {
|
||||
// Use pre-defined payment method IDs for special test cards
|
||||
@@ -174,6 +174,18 @@ export const attachPaymentMethod = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Alipay case - create and attach alipay payment method
|
||||
if (type === "alipay") {
|
||||
const pm = await stripeCli.paymentMethods.create({
|
||||
type: "alipay",
|
||||
});
|
||||
|
||||
await stripeCli.paymentMethods.attach(pm.id, {
|
||||
customer: stripeCusId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Success case - create from token
|
||||
const pm = await stripeCli.paymentMethods.create({
|
||||
type: "card",
|
||||
|
||||
@@ -3,45 +3,67 @@ import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import { removeAllPaymentMethods } from "@/external/stripe/customers/paymentMethods/operations/removeAllPaymentMethods";
|
||||
import { attachPaymentMethod } from "@/utils/scriptUtils/initCustomer";
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("temp: attach free default, attach pro annual, cancel immediately")}`, async () => {
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
|
||||
const free = products.base({
|
||||
id: "free",
|
||||
items: [messagesItem],
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
const proAnnual = products.proAnnual({
|
||||
id: "pro-annual",
|
||||
const pro = products.pro({
|
||||
id: "pro",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const { customerId, autumnV1 } = await initScenario({
|
||||
const premium = products.base({
|
||||
id: "premium",
|
||||
items: [items.monthlyPrice({ price: 50 }), messagesItem],
|
||||
});
|
||||
|
||||
const { customerId, autumnV1, ctx, customer } = await initScenario({
|
||||
customerId: "temp-free-pro-annual-cancel",
|
||||
setup: [
|
||||
s.customer({ paymentMethod: "success", withDefault: true }),
|
||||
s.products({ list: [free, proAnnual] }),
|
||||
],
|
||||
actions: [
|
||||
// Attach pro annual (free default already attached via withDefault: true)
|
||||
s.attach({ productId: proAnnual.id }),
|
||||
s.customer({ withDefault: true, paymentMethod: "success" }),
|
||||
s.products({ list: [pro, premium] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
// Verify pro annual is active
|
||||
const customerAfterAttach = await autumnV1.customers.get(customerId);
|
||||
console.log("Customer after attach:", JSON.stringify(customerAfterAttach.products, null, 2));
|
||||
|
||||
// Cancel pro annual immediately
|
||||
await autumnV1.subscriptions.update({
|
||||
// Now try to attach pro annual (this should trigger the Alipay handling code)
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proAnnual.id,
|
||||
cancel_action: "cancel_immediately",
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
// Verify state after cancel
|
||||
const customerAfterCancel = await autumnV1.customers.get(customerId);
|
||||
console.log("Customer after cancel:", JSON.stringify(customerAfterCancel.products, null, 2));
|
||||
await removeAllPaymentMethods({
|
||||
stripeClient: ctx.stripeCli,
|
||||
stripeCustomerId: customer.processor?.id,
|
||||
});
|
||||
|
||||
await attachPaymentMethod({
|
||||
stripeCli: ctx.stripeCli,
|
||||
stripeCusId: customer.processor?.id,
|
||||
type: "alipay",
|
||||
});
|
||||
|
||||
const res = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
|
||||
console.log("Res:", res);
|
||||
|
||||
// // Verify pro annual is active
|
||||
// const customerAfterAttach = await autumnV1.customers.get(customerId);
|
||||
// console.log("Customer after attach:", JSON.stringify(customerAfterAttach.products, null, 2));
|
||||
|
||||
// // Cancel pro annual immediately
|
||||
// await autumnV1.subscriptions.update({
|
||||
// customer_id: customerId,
|
||||
// product_id: proAnnual.id,
|
||||
// cancel_action: "cancel_immediately",
|
||||
// });
|
||||
|
||||
// // Verify state after cancel
|
||||
// const customerAfterCancel = await autumnV1.customers.get(customerId);
|
||||
// console.log("Customer after cancel:", JSON.stringify(customerAfterCancel.products, null, 2));
|
||||
});
|
||||
|
||||
@@ -93,7 +93,6 @@ describe(`${chalk.yellowBright("basic2: Testing attach monthly add on")}`, () =>
|
||||
cusRes: res,
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
const monthlyQuantity = 500;
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
|
||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||
import { advanceToNextInvoice } from "@tests/utils/testAttachUtils/testAttachUtils";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { addHours, addMonths } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
@@ -76,7 +74,7 @@ describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () =>
|
||||
});
|
||||
|
||||
test("should attach pro product and switch to failed payment method", async () => {
|
||||
await autumnV1.attach({
|
||||
const res = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proProd.id,
|
||||
});
|
||||
@@ -88,14 +86,9 @@ describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () =>
|
||||
});
|
||||
|
||||
test("should advance to next cycle", async () => {
|
||||
await advanceTestClock({
|
||||
await advanceToNextInvoice({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addHours(
|
||||
addMonths(new Date(), 1),
|
||||
hoursToFinalizeInvoice,
|
||||
).getTime(),
|
||||
waitForSeconds: 30,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { LegacyVersion } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.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 { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout.js";
|
||||
@@ -81,25 +80,14 @@ describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout via checko
|
||||
});
|
||||
|
||||
test("should have no URL returned if try to attach premium (with invoice true)", async () => {
|
||||
await expectAutumnError({
|
||||
func: async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
invoice: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const res = await autumn.checkout({
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
invoice: true,
|
||||
});
|
||||
|
||||
expect(res.url).toBeNull();
|
||||
console.log("Res:", res);
|
||||
});
|
||||
return;
|
||||
|
||||
test("should attach premium product via invoice enable immediately", async () => {
|
||||
const res = await autumn.attach({
|
||||
|
||||
@@ -136,6 +136,7 @@ describe(`${chalk.yellowBright("upgradeOld4: Testing upgrade from pro -> premium
|
||||
},
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
// Attach payment method
|
||||
test("should attach successful payment method", async () => {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import { removeAllPaymentMethods } from "@/external/stripe/customers/paymentMethods/operations/removeAllPaymentMethods.js";
|
||||
import { attachPaymentMethod } from "@/utils/scriptUtils/initCustomer.js";
|
||||
|
||||
test.concurrent("should attach product with alipay payment method and complete invoice", async () => {
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
|
||||
const pro = products.pro({
|
||||
id: "pro",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const { customerId, autumnV1 } = await initScenario({
|
||||
customerId: "alipay-attach-1",
|
||||
setup: [
|
||||
s.customer({ withDefault: false, paymentMethod: "alipay" }),
|
||||
s.products({ list: [pro] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
// Attach pro with alipay - should return checkout_url since alipay requires redirect
|
||||
const res = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
expect(res.checkout_url).toBeDefined();
|
||||
expect(res.checkout_url).toContain("checkout.stripe.com");
|
||||
});
|
||||
|
||||
test.concurrent("should attach pro, switch to alipay, add premium addon, and complete invoice", async () => {
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
|
||||
const pro = products.pro({
|
||||
id: "pro",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
// Premium is an addon (different group) so both can be active
|
||||
const premium = products.base({
|
||||
id: "premium",
|
||||
items: [messagesItem, items.monthlyPrice({ price: 50 })],
|
||||
});
|
||||
|
||||
const { customerId, autumnV1, ctx, customer } = await initScenario({
|
||||
customerId: "alipay-addon-2",
|
||||
setup: [
|
||||
s.customer({ withDefault: false, paymentMethod: "success" }),
|
||||
s.products({ list: [pro, premium] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
// Attach pro with card payment method
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
// Verify pro is attached
|
||||
const customerAfterPro = await autumnV1.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer: customerAfterPro,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
// Remove all payment methods and attach alipay
|
||||
const stripeCustomerId = customer.processor?.id;
|
||||
if (!stripeCustomerId) throw new Error("No stripe customer id");
|
||||
|
||||
await removeAllPaymentMethods({
|
||||
stripeClient: ctx.stripeCli,
|
||||
stripeCustomerId,
|
||||
});
|
||||
|
||||
await attachPaymentMethod({
|
||||
stripeCli: ctx.stripeCli,
|
||||
stripeCusId: stripeCustomerId,
|
||||
type: "alipay",
|
||||
});
|
||||
|
||||
// Add premium with alipay - should return checkout_url
|
||||
const premiumRes = await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
|
||||
// If checkout_url is returned, complete the invoice confirmation
|
||||
if (premiumRes.checkout_url) {
|
||||
await completeInvoiceCheckout({
|
||||
url: premiumRes.checkout_url,
|
||||
});
|
||||
}
|
||||
|
||||
// Verify both pro and premium are attached
|
||||
const customerAfterAddon = await autumnV1.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
customer: customerAfterAddon,
|
||||
product: premium,
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ type AdvanceClockAction = {
|
||||
|
||||
type AttachPaymentMethodAction = {
|
||||
type: "attachPaymentMethod";
|
||||
paymentMethodType: "success" | "fail" | "authenticate";
|
||||
paymentMethodType: "success" | "fail" | "authenticate" | "alipay";
|
||||
};
|
||||
|
||||
type RemovePaymentMethodAction = {
|
||||
@@ -103,7 +103,7 @@ type CleanupConfig = {
|
||||
|
||||
type ScenarioConfig = {
|
||||
testClock: boolean;
|
||||
attachPm?: "success" | "fail" | "authenticate";
|
||||
attachPm?: "success" | "fail" | "authenticate" | "alipay";
|
||||
customerData?: CustomerData;
|
||||
withDefault: boolean;
|
||||
defaultGroup?: string;
|
||||
@@ -160,7 +160,7 @@ const customer = ({
|
||||
skipWebhooks,
|
||||
}: {
|
||||
testClock?: boolean;
|
||||
paymentMethod?: "success" | "fail" | "authenticate";
|
||||
paymentMethod?: "success" | "fail" | "authenticate" | "alipay";
|
||||
data?: CustomerData;
|
||||
withDefault?: boolean;
|
||||
defaultGroup?: string;
|
||||
@@ -353,7 +353,7 @@ const advanceTestClock = ({
|
||||
const attachPaymentMethod = ({
|
||||
type,
|
||||
}: {
|
||||
type: "success" | "fail" | "authenticate";
|
||||
type: "success" | "fail" | "authenticate" | "alipay";
|
||||
}): ConfigFn => {
|
||||
return (config) => ({
|
||||
...config,
|
||||
|
||||
Reference in New Issue
Block a user