Files
cfw-autumn/server/tests/integration/billing/update-subscription/refund-behavior/refund-payment-method.test.ts
2026-01-23 16:57:04 +00:00

194 lines
6.8 KiB
TypeScript

import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
import { createStripeCli } from "@/external/connect/createStripeCli";
/**
* Refund Behavior: refund_payment_method Tests
*
* Tests for refund_behavior: 'refund_payment_method' which issues a refund
* to the customer's payment method when the invoice total is negative.
*
* Key behaviors:
* - Only applies when invoice total is negative (downgrade scenarios)
* - Refund is issued via Stripe to the original payment method
* - Default behavior (grant_invoice_credits) applies credits to customer balance instead
*/
// ═══════════════════════════════════════════════════════════════════════════════
// REFUND ON DOWNGRADE
// ═══════════════════════════════════════════════════════════════════════════════
test.skip(`${chalk.yellowBright("refund_payment_method: price decrease issues refund")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItem = items.monthlyPrice({ price: 30 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItem] });
const { customerId, autumnV1, ctx, customer } = await initScenario({
customerId: "rb-refund-price-dec",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [s.attach({ productId: "pro" })],
});
const stripeCustomerId = customer?.processor?.id;
expect(stripeCustomerId).toBeDefined();
const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env });
// Decrease price from $30 to $20 (should create negative invoice / refund)
const newPriceItem = items.monthlyPrice({ price: 20 });
const updateParams = {
customer_id: customerId,
product_id: pro.id,
items: [messagesItem, newPriceItem],
refund_behavior: "refund_payment_method" as const,
};
// Preview shows negative total (credit)
const preview = await autumnV1.subscriptions.previewUpdate(updateParams);
expect(preview.total).toBe(-10); // $20 - $30 = -$10
// Execute update with refund_payment_method
await autumnV1.subscriptions.update(updateParams);
// Wait for Stripe to process
await new Promise((resolve) => setTimeout(resolve, 1000));
// Get refunds to verify refund was issued
const refunds = await stripeCli.refunds.list({
limit: 10,
});
// Find the refund we just created (should be $10 = 1000 cents)
const ourRefund = refunds.data.find(
(r: { amount: number }) => r.amount === 1000,
);
expect(ourRefund).toBeDefined();
expect(ourRefund!.status).toBe("succeeded");
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerInvoiceCorrect({
customer: customerAfter,
count: 2,
latestTotal: -10,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});
test.skip(`${chalk.yellowBright("refund_payment_method: quantity decrease issues refund")}`, async () => {
const billingUnits = 1;
const pricePerUnit = 10;
const prepaidItem = items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit,
});
const pro = products.base({ id: "pro", items: [prepaidItem] });
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "rb-refund-qty-dec",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [
s.attach({
productId: "pro",
options: [{ feature_id: TestFeature.Messages, quantity: 10 }],
}),
],
});
// Decrease quantity from 10 to 5 (should create negative invoice)
const updateParams = {
customer_id: customerId,
product_id: pro.id,
options: [{ feature_id: TestFeature.Messages, quantity: 5 }],
refund_behavior: "refund_payment_method" as const,
};
// Preview shows negative total (credit)
const preview = await autumnV1.subscriptions.previewUpdate(updateParams);
expect(preview.total).toBeLessThan(0); // Should be negative
// Execute update with refund_payment_method
await autumnV1.subscriptions.update(updateParams);
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Entitlements should be updated to 5
expect(customer.features?.[TestFeature.Messages]?.balance).toBe(5);
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// DEFAULT BEHAVIOR: grant_invoice_credits
// ═══════════════════════════════════════════════════════════════════════════════
test.skip(`${chalk.yellowBright("grant_invoice_credits: default behavior on downgrade")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const priceItem = items.monthlyPrice({ price: 30 });
const pro = products.base({ id: "pro", items: [messagesItem, priceItem] });
const { customerId, autumnV1, ctx } = await initScenario({
customerId: "rb-credits-default",
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [s.attach({ productId: "pro" })],
});
// Decrease price from $30 to $20
const newPriceItem = items.monthlyPrice({ price: 20 });
const updateParams = {
customer_id: customerId,
product_id: pro.id,
items: [messagesItem, newPriceItem],
// No refund_behavior specified - defaults to grant_invoice_credits
};
// Preview shows negative total
const preview = await autumnV1.subscriptions.previewUpdate(updateParams);
expect(preview.total).toBe(-10);
// Execute update (default: grant_invoice_credits)
await autumnV1.subscriptions.update(updateParams);
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerInvoiceCorrect({
customer,
count: 2,
});
await expectSubToBeCorrect({
db: ctx.db,
customerId,
org: ctx.org,
env: ctx.env,
});
});