mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(api): mirror Stripe billing data using the shapes the pinned API version sends (#1708)
This commit is contained in:
1 parent
1922d56188
commit
7e1ca9ed6a
13 files changed
+160
-153
No files matched your search
@@ -742,6 +742,9 @@ class BillingAdminControllerService {
|
|||||||
refundTarget,
|
refundTarget,
|
||||||
subscription,
|
subscription,
|
||||||
});
|
});
|
||||||
|
if (refundDecision.amountCents !== null && !refundTarget) {
|
||||||
|
throw new StripeError('No paid Stripe invoice with a refundable payment was found for this subscription');
|
||||||
|
}
|
||||||
const intentId = await this.billingRepository.actionIntents.create({
|
const intentId = await this.billingRepository.actionIntents.create({
|
||||||
userId: BigInt(syncedTargetUser.id),
|
userId: BigInt(syncedTargetUser.id),
|
||||||
actorAdminId: BigInt(params.adminUserId),
|
actorAdminId: BigInt(params.adminUserId),
|
||||||
@@ -758,10 +761,7 @@ class BillingAdminControllerService {
|
|||||||
await this.billingRepository.actionIntents.markStage(intentId, 'sub_canceled', {
|
await this.billingRepository.actionIntents.markStage(intentId, 'sub_canceled', {
|
||||||
sub_canceled_at: new Date(),
|
sub_canceled_at: new Date(),
|
||||||
});
|
});
|
||||||
if (refundDecision.amountCents !== null) {
|
if (refundDecision.amountCents !== null && refundTarget) {
|
||||||
if (!refundTarget) {
|
|
||||||
throw new StripeError('No paid Stripe invoice with a refundable payment was found for this subscription');
|
|
||||||
}
|
|
||||||
refund = await this.stripe.refunds.create(
|
refund = await this.stripe.refunds.create(
|
||||||
{
|
{
|
||||||
...(refundTarget.paymentIntentId
|
...(refundTarget.paymentIntentId
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ describe('Admin billing overview', () => {
|
|||||||
invoices: {
|
invoices: {
|
||||||
[params.invoiceId]: {
|
[params.invoiceId]: {
|
||||||
customer: params.stripeCustomerId,
|
customer: params.stripeCustomerId,
|
||||||
subscription: params.stripeSubscriptionId,
|
subscriptionId: params.stripeSubscriptionId,
|
||||||
amount_due: params.amountPaidCents,
|
amount_due: params.amountPaidCents,
|
||||||
amount_paid: params.amountPaidCents,
|
amount_paid: params.amountPaidCents,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
@@ -359,7 +359,7 @@ describe('Admin billing overview', () => {
|
|||||||
invoices: {
|
invoices: {
|
||||||
in_local_checkout_1: {
|
in_local_checkout_1: {
|
||||||
customer: stripeCustomerId,
|
customer: stripeCustomerId,
|
||||||
subscription: 'sub_billing_target',
|
subscriptionId: 'sub_billing_target',
|
||||||
amount_due: 499,
|
amount_due: 499,
|
||||||
amount_paid: 499,
|
amount_paid: 499,
|
||||||
billing_reason: 'subscription_create',
|
billing_reason: 'subscription_create',
|
||||||
@@ -396,7 +396,7 @@ describe('Admin billing overview', () => {
|
|||||||
},
|
},
|
||||||
in_renewal_1: {
|
in_renewal_1: {
|
||||||
customer: stripeCustomerId,
|
customer: stripeCustomerId,
|
||||||
subscription: 'sub_billing_target',
|
subscriptionId: 'sub_billing_target',
|
||||||
amount_due: 499,
|
amount_due: 499,
|
||||||
amount_paid: 499,
|
amount_paid: 499,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
@@ -545,7 +545,7 @@ describe('Admin billing overview', () => {
|
|||||||
invoices: {
|
invoices: {
|
||||||
[invoiceId]: {
|
[invoiceId]: {
|
||||||
customer: stripeCustomerId,
|
customer: stripeCustomerId,
|
||||||
subscription: stripeSubscriptionId,
|
subscriptionId: stripeSubscriptionId,
|
||||||
amount_due: 499,
|
amount_due: 499,
|
||||||
amount_paid: 499,
|
amount_paid: 499,
|
||||||
billing_reason: 'subscription_create',
|
billing_reason: 'subscription_create',
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ describe('mapStripeInvoiceToRow', () => {
|
|||||||
const inv = stripeFixture<Stripe.Invoice>({
|
const inv = stripeFixture<Stripe.Invoice>({
|
||||||
id: 'in_1',
|
id: 'in_1',
|
||||||
customer: 'cus_1',
|
customer: 'cus_1',
|
||||||
subscription: 'sub_1',
|
parent: {type: 'subscription_details', subscription_details: {subscription: 'sub_1'}},
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
number: 'INV-001',
|
number: 'INV-001',
|
||||||
currency: 'usd',
|
currency: 'usd',
|
||||||
|
|||||||
@@ -94,28 +94,6 @@ export interface StripeSubscriptionPayload {
|
|||||||
trial_start?: number | null;
|
trial_start?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StripeInvoiceCompatibility extends Stripe.Invoice {
|
|
||||||
subscription?: StripeExpandableId;
|
|
||||||
tax?: number | null;
|
|
||||||
application_fee_amount?: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StripePaymentIntentCompatibility extends Stripe.PaymentIntent {
|
|
||||||
invoice?: StripeExpandableId;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StripeChargeCompatibility extends Stripe.Charge {
|
|
||||||
invoice?: StripeExpandableId;
|
|
||||||
}
|
|
||||||
|
|
||||||
type StripeRefundCompatibility = Stripe.Refund & {
|
|
||||||
livemode?: boolean | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface StripeCheckoutSessionCompatibility extends Stripe.Checkout.Session {
|
|
||||||
completed_at?: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBillingSubscriptionItemValue(
|
function createBillingSubscriptionItemValue(
|
||||||
itemId: string,
|
itemId: string,
|
||||||
priceId: string,
|
priceId: string,
|
||||||
@@ -280,6 +258,14 @@ export function computeStripeUpdatedAt(
|
|||||||
return new Date(max * 1000);
|
return new Date(max * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sumInvoiceTotalTaxes(inv: Stripe.Invoice): bigint | null {
|
||||||
|
const taxes = inv.total_taxes ?? null;
|
||||||
|
if (taxes === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return BigInt(taxes.reduce((total, tax) => total + (tax.amount ?? 0), 0));
|
||||||
|
}
|
||||||
|
|
||||||
function idOf(
|
function idOf(
|
||||||
ref:
|
ref:
|
||||||
| string
|
| string
|
||||||
@@ -689,9 +675,7 @@ export function mapStripeInvoiceToRow(
|
|||||||
throw new BillingMappingError('Invoice is missing customer', inv.id);
|
throw new BillingMappingError('Invoice is missing customer', inv.id);
|
||||||
}
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const invoice = inv as StripeInvoiceCompatibility;
|
const subscriptionId = idOf(inv.parent?.subscription_details?.subscription ?? null);
|
||||||
const subscriptionId =
|
|
||||||
idOf(invoice.subscription ?? null) ?? idOf(invoice.parent?.subscription_details?.subscription ?? null);
|
|
||||||
const userIdFromMetadata = parseUserIdFromMetadata(inv.metadata);
|
const userIdFromMetadata = parseUserIdFromMetadata(inv.metadata);
|
||||||
const userId = hints?.knownUserId ?? userIdFromMetadata;
|
const userId = hints?.knownUserId ?? userIdFromMetadata;
|
||||||
const transitions = inv.status_transitions ?? null;
|
const transitions = inv.status_transitions ?? null;
|
||||||
@@ -718,12 +702,11 @@ export function mapStripeInvoiceToRow(
|
|||||||
amount_paid: typeof inv.amount_paid === 'number' ? BigInt(inv.amount_paid) : null,
|
amount_paid: typeof inv.amount_paid === 'number' ? BigInt(inv.amount_paid) : null,
|
||||||
amount_remaining: typeof inv.amount_remaining === 'number' ? BigInt(inv.amount_remaining) : null,
|
amount_remaining: typeof inv.amount_remaining === 'number' ? BigInt(inv.amount_remaining) : null,
|
||||||
subtotal: typeof inv.subtotal === 'number' ? BigInt(inv.subtotal) : null,
|
subtotal: typeof inv.subtotal === 'number' ? BigInt(inv.subtotal) : null,
|
||||||
tax: typeof invoice.tax === 'number' ? BigInt(invoice.tax) : null,
|
tax: sumInvoiceTotalTaxes(inv),
|
||||||
total: typeof inv.total === 'number' ? BigInt(inv.total) : null,
|
total: typeof inv.total === 'number' ? BigInt(inv.total) : null,
|
||||||
starting_balance: typeof inv.starting_balance === 'number' ? BigInt(inv.starting_balance) : null,
|
starting_balance: typeof inv.starting_balance === 'number' ? BigInt(inv.starting_balance) : null,
|
||||||
ending_balance: typeof inv.ending_balance === 'number' ? BigInt(inv.ending_balance) : null,
|
ending_balance: typeof inv.ending_balance === 'number' ? BigInt(inv.ending_balance) : null,
|
||||||
application_fee_amount:
|
application_fee_amount: null,
|
||||||
typeof invoice.application_fee_amount === 'number' ? BigInt(invoice.application_fee_amount) : null,
|
|
||||||
attempt_count: inv.attempt_count ?? null,
|
attempt_count: inv.attempt_count ?? null,
|
||||||
attempted: inv.attempted ?? null,
|
attempted: inv.attempted ?? null,
|
||||||
auto_advance: inv.auto_advance ?? null,
|
auto_advance: inv.auto_advance ?? null,
|
||||||
@@ -818,7 +801,6 @@ export function mapStripePaymentIntentToRow(pi: Stripe.PaymentIntent): {
|
|||||||
throw new BillingMappingError('PaymentIntent is missing id');
|
throw new BillingMappingError('PaymentIntent is missing id');
|
||||||
}
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const paymentIntent = pi as StripePaymentIntentCompatibility;
|
|
||||||
const customerId = idOf(
|
const customerId = idOf(
|
||||||
pi.customer as
|
pi.customer as
|
||||||
| string
|
| string
|
||||||
@@ -828,7 +810,7 @@ export function mapStripePaymentIntentToRow(pi: Stripe.PaymentIntent): {
|
|||||||
| null
|
| null
|
||||||
| undefined,
|
| undefined,
|
||||||
);
|
);
|
||||||
const invoiceId = idOf(paymentIntent.invoice ?? null);
|
const invoiceId = null;
|
||||||
const paymentMethodId = idOf(
|
const paymentMethodId = idOf(
|
||||||
pi.payment_method as
|
pi.payment_method as
|
||||||
| string
|
| string
|
||||||
@@ -892,7 +874,6 @@ export function mapStripeChargeToRow(c: Stripe.Charge): {
|
|||||||
throw new BillingMappingError('Charge is missing id');
|
throw new BillingMappingError('Charge is missing id');
|
||||||
}
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const charge = c as StripeChargeCompatibility;
|
|
||||||
const customerId = idOf(
|
const customerId = idOf(
|
||||||
c.customer as
|
c.customer as
|
||||||
| string
|
| string
|
||||||
@@ -911,7 +892,7 @@ export function mapStripeChargeToRow(c: Stripe.Charge): {
|
|||||||
| null
|
| null
|
||||||
| undefined,
|
| undefined,
|
||||||
);
|
);
|
||||||
const invoiceId = idOf(charge.invoice ?? null);
|
const invoiceId = null;
|
||||||
const paymentMethodId = c.payment_method ?? null;
|
const paymentMethodId = c.payment_method ?? null;
|
||||||
const card = c.payment_method_details?.card ?? null;
|
const card = c.payment_method_details?.card ?? null;
|
||||||
const billingDetails = c.billing_details ?? null;
|
const billingDetails = c.billing_details ?? null;
|
||||||
@@ -974,6 +955,7 @@ export function mapStripeRefundToRow(
|
|||||||
invoiceId?: string;
|
invoiceId?: string;
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
userId?: bigint;
|
userId?: bigint;
|
||||||
|
livemode?: boolean;
|
||||||
},
|
},
|
||||||
): {
|
): {
|
||||||
primary: BillingRefundRow;
|
primary: BillingRefundRow;
|
||||||
@@ -990,7 +972,6 @@ export function mapStripeRefundToRow(
|
|||||||
const invoiceId = hints?.invoiceId ?? null;
|
const invoiceId = hints?.invoiceId ?? null;
|
||||||
const customerId = hints?.customerId ?? null;
|
const customerId = hints?.customerId ?? null;
|
||||||
const userId = hints?.userId ?? null;
|
const userId = hints?.userId ?? null;
|
||||||
const refund = r as StripeRefundCompatibility;
|
|
||||||
const primary: BillingRefundRow = {
|
const primary: BillingRefundRow = {
|
||||||
provider_id: r.id,
|
provider_id: r.id,
|
||||||
charge_id: chargeId,
|
charge_id: chargeId,
|
||||||
@@ -1005,7 +986,7 @@ export function mapStripeRefundToRow(
|
|||||||
receipt_number: r.receipt_number ?? null,
|
receipt_number: r.receipt_number ?? null,
|
||||||
failure_reason: r.failure_reason ?? null,
|
failure_reason: r.failure_reason ?? null,
|
||||||
description: r.description ?? null,
|
description: r.description ?? null,
|
||||||
livemode: refund.livemode ?? null,
|
livemode: hints?.livemode ?? null,
|
||||||
metadata: safeMetadata(r.metadata),
|
metadata: safeMetadata(r.metadata),
|
||||||
stripe_created_at: unixToDate(r.created),
|
stripe_created_at: unixToDate(r.created),
|
||||||
stripe_updated_at: computeStripeUpdatedAt({created: r.created}),
|
stripe_updated_at: computeStripeUpdatedAt({created: r.created}),
|
||||||
@@ -1049,16 +1030,17 @@ export function mapStripeCheckoutSessionToRow(
|
|||||||
cs: Stripe.Checkout.Session,
|
cs: Stripe.Checkout.Session,
|
||||||
hints?: {
|
hints?: {
|
||||||
knownUserId?: bigint;
|
knownUserId?: bigint;
|
||||||
|
eventCreated?: number;
|
||||||
},
|
},
|
||||||
): {
|
): {
|
||||||
primary: BillingCheckoutSessionRow;
|
primary: BillingCheckoutSessionRow;
|
||||||
byCustomer: BillingCheckoutSessionByCustomerRow | null;
|
byCustomer: BillingCheckoutSessionByCustomerRow | null;
|
||||||
} {
|
} {
|
||||||
|
const completedAt = cs.status === 'complete' ? (hints?.eventCreated ?? null) : null;
|
||||||
if (!cs.id) {
|
if (!cs.id) {
|
||||||
throw new BillingMappingError('Checkout session is missing id');
|
throw new BillingMappingError('Checkout session is missing id');
|
||||||
}
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const checkoutSession = cs as StripeCheckoutSessionCompatibility;
|
|
||||||
const customerId = idOf(
|
const customerId = idOf(
|
||||||
cs.customer as
|
cs.customer as
|
||||||
| string
|
| string
|
||||||
@@ -1124,14 +1106,14 @@ export function mapStripeCheckoutSessionToRow(
|
|||||||
amount_total: typeof cs.amount_total === 'number' ? BigInt(cs.amount_total) : null,
|
amount_total: typeof cs.amount_total === 'number' ? BigInt(cs.amount_total) : null,
|
||||||
currency: cs.currency ?? null,
|
currency: cs.currency ?? null,
|
||||||
expires_at: unixToDate(cs.expires_at),
|
expires_at: unixToDate(cs.expires_at),
|
||||||
completed_at: unixToDate(checkoutSession.completed_at ?? null),
|
completed_at: completedAt === null ? null : unixToDate(completedAt),
|
||||||
livemode: cs.livemode ?? null,
|
livemode: cs.livemode ?? null,
|
||||||
client_reference_id: cs.client_reference_id ?? null,
|
client_reference_id: cs.client_reference_id ?? null,
|
||||||
metadata: safeMetadata(cs.metadata),
|
metadata: safeMetadata(cs.metadata),
|
||||||
stripe_created_at: unixToDate(cs.created),
|
stripe_created_at: unixToDate(cs.created),
|
||||||
stripe_updated_at: computeStripeUpdatedAt({
|
stripe_updated_at: computeStripeUpdatedAt({
|
||||||
created: cs.created,
|
created: cs.created,
|
||||||
completed_at: checkoutSession.completed_at ?? null,
|
completed_at: completedAt,
|
||||||
}),
|
}),
|
||||||
mirrored_at: now,
|
mirrored_at: now,
|
||||||
} as BillingCheckoutSessionRow;
|
} as BillingCheckoutSessionRow;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class BillingCheckoutSessionRepository {
|
|||||||
cs: Stripe.Checkout.Session,
|
cs: Stripe.Checkout.Session,
|
||||||
hints?: {
|
hints?: {
|
||||||
knownUserId?: bigint;
|
knownUserId?: bigint;
|
||||||
|
eventCreated?: number;
|
||||||
},
|
},
|
||||||
): Promise<{
|
): Promise<{
|
||||||
changed: boolean;
|
changed: boolean;
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export class BillingRefundRepository {
|
|||||||
invoiceId?: string;
|
invoiceId?: string;
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
userId?: bigint;
|
userId?: bigint;
|
||||||
|
livemode?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<{
|
): Promise<{
|
||||||
changed: boolean;
|
changed: boolean;
|
||||||
|
|||||||
@@ -115,15 +115,3 @@ export function getFirstInvoicePaymentIntentId(invoice: Stripe.Invoice | null):
|
|||||||
}
|
}
|
||||||
return paymentIntent.id ?? null;
|
return paymentIntent.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFirstInvoicePaymentIntentLatestChargeId(invoice: Stripe.Invoice | null): string | null {
|
|
||||||
const paymentIntent = getFirstInvoicePaymentIntent(invoice);
|
|
||||||
if (!paymentIntent || typeof paymentIntent === 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const latestCharge = paymentIntent.latest_charge ?? null;
|
|
||||||
if (typeof latestCharge === 'string') {
|
|
||||||
return latestCharge;
|
|
||||||
}
|
|
||||||
return latestCharge?.id ?? null;
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,6 @@ interface RefundTarget {
|
|||||||
|
|
||||||
type StripeInvoiceWithPayments = Stripe.Invoice & {
|
type StripeInvoiceWithPayments = Stripe.Invoice & {
|
||||||
customer?: string | Stripe.Customer | null;
|
customer?: string | Stripe.Customer | null;
|
||||||
subscription?: string | Stripe.Subscription | null;
|
|
||||||
payments?: {
|
payments?: {
|
||||||
data?: Array<{
|
data?: Array<{
|
||||||
payment?: {
|
payment?: {
|
||||||
@@ -54,6 +53,10 @@ type StripeInvoiceWithPayments = Stripe.Invoice & {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveInvoiceSubscriptionId(invoice: Stripe.Invoice): string | null {
|
||||||
|
return extractId(invoice.parent?.subscription_details?.subscription ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
function getInvoicePaymentRef(invoice: Stripe.Invoice): {
|
function getInvoicePaymentRef(invoice: Stripe.Invoice): {
|
||||||
chargeId: string | null;
|
chargeId: string | null;
|
||||||
paymentIntentId: string | null;
|
paymentIntentId: string | null;
|
||||||
@@ -132,7 +135,7 @@ export class StripeRefundService {
|
|||||||
chargeId: ref.chargeId,
|
chargeId: ref.chargeId,
|
||||||
paymentIntentId: ref.paymentIntentId,
|
paymentIntentId: ref.paymentIntentId,
|
||||||
paidAt,
|
paidAt,
|
||||||
subscriptionId: extractId((invoice as StripeInvoiceWithPayments).subscription),
|
subscriptionId: resolveInvoiceSubscriptionId(invoice),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -260,37 +260,13 @@ export class StripeSubscriptionReconciler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPriceIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
getPriceIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||||
type InvoiceLineWithPrice = Stripe.InvoiceLineItem & {
|
|
||||||
price?: string | Stripe.Price | null;
|
|
||||||
pricing?: {
|
|
||||||
price_details?: {
|
|
||||||
price?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
parent?: {
|
|
||||||
subscription_item_details?: {
|
|
||||||
price?: {
|
|
||||||
price?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (!invoice.lines?.data?.length) {
|
if (!invoice.lines?.data?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (const line of invoice.lines.data) {
|
for (const line of invoice.lines.data) {
|
||||||
const lineWithPrice = line as InvoiceLineWithPrice;
|
const priceId = extractId(line.pricing?.price_details?.price ?? null);
|
||||||
const directPriceId = extractId(lineWithPrice.price);
|
if (priceId) {
|
||||||
if (directPriceId) {
|
return priceId;
|
||||||
return directPriceId;
|
|
||||||
}
|
|
||||||
const nestedPriceId = lineWithPrice.pricing?.price_details?.price;
|
|
||||||
if (nestedPriceId) {
|
|
||||||
return extractId(nestedPriceId);
|
|
||||||
}
|
|
||||||
const parentNestedPriceId = lineWithPrice.parent?.subscription_item_details?.price?.price;
|
|
||||||
if (parentNestedPriceId) {
|
|
||||||
return extractId(parentNestedPriceId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -328,39 +304,15 @@ export class StripeSubscriptionReconciler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||||
type InvoiceWithSubscription = Stripe.Invoice & {
|
const parentSubscription = extractId(invoice.parent?.subscription_details?.subscription ?? null);
|
||||||
subscription?: string | Stripe.Subscription;
|
|
||||||
};
|
|
||||||
const invoiceWithSubscription = invoice as InvoiceWithSubscription;
|
|
||||||
const directSubscription = invoiceWithSubscription.subscription;
|
|
||||||
if (directSubscription) {
|
|
||||||
return extractId(directSubscription);
|
|
||||||
}
|
|
||||||
type InvoiceWithParent = Stripe.Invoice & {
|
|
||||||
parent?: {
|
|
||||||
subscription_details?: {
|
|
||||||
subscription?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
type InvoiceLineWithParent = Stripe.InvoiceLineItem & {
|
|
||||||
parent?: {
|
|
||||||
subscription_item_details?: {
|
|
||||||
subscription?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const invoiceWithParent = invoice as InvoiceWithParent;
|
|
||||||
const parentSubscription = invoiceWithParent.parent?.subscription_details?.subscription;
|
|
||||||
if (parentSubscription) {
|
if (parentSubscription) {
|
||||||
return extractId(parentSubscription);
|
return parentSubscription;
|
||||||
}
|
}
|
||||||
if (invoice.lines?.data?.length) {
|
if (invoice.lines?.data?.length) {
|
||||||
for (const line of invoice.lines.data) {
|
for (const line of invoice.lines.data) {
|
||||||
const lineWithParent = line as InvoiceLineWithParent;
|
const subscriptionId = line.parent?.subscription_item_details?.subscription ?? null;
|
||||||
const subscriptionId = lineWithParent.parent?.subscription_item_details?.subscription;
|
|
||||||
if (subscriptionId) {
|
if (subscriptionId) {
|
||||||
return extractId(subscriptionId);
|
return subscriptionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class StripeWebhookService {
|
|||||||
case 'checkout.session.completed': {
|
case 'checkout.session.completed': {
|
||||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||||
await this.safeMirrorUpsert(event, () =>
|
await this.safeMirrorUpsert(event, () =>
|
||||||
this.billingRepository.checkoutSessions.upsertFromStripe(checkoutSession),
|
this.billingRepository.checkoutSessions.upsertFromStripe(checkoutSession, {eventCreated: event.created}),
|
||||||
);
|
);
|
||||||
if (checkoutSession.metadata?.verification_type === 'uk_age_verification' && this.ageVerificationService) {
|
if (checkoutSession.metadata?.verification_type === 'uk_age_verification' && this.ageVerificationService) {
|
||||||
await this.ageVerificationService.completeVerification(checkoutSession);
|
await this.ageVerificationService.completeVerification(checkoutSession);
|
||||||
@@ -165,44 +165,48 @@ export class StripeWebhookService {
|
|||||||
}
|
}
|
||||||
case 'checkout.session.async_payment_succeeded': {
|
case 'checkout.session.async_payment_succeeded': {
|
||||||
const cs = event.data.object as Stripe.Checkout.Session;
|
const cs = event.data.object as Stripe.Checkout.Session;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.checkoutSessions.upsertFromStripe(cs));
|
await this.safeMirrorUpsert(event, () =>
|
||||||
|
this.billingRepository.checkoutSessions.upsertFromStripe(cs, {eventCreated: event.created}),
|
||||||
|
);
|
||||||
await this.checkoutHandler.handleAsyncPaymentSucceeded(cs);
|
await this.checkoutHandler.handleAsyncPaymentSucceeded(cs);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'checkout.session.async_payment_failed': {
|
case 'checkout.session.async_payment_failed': {
|
||||||
const cs = event.data.object as Stripe.Checkout.Session;
|
const cs = event.data.object as Stripe.Checkout.Session;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.checkoutSessions.upsertFromStripe(cs));
|
await this.safeMirrorUpsert(event, () =>
|
||||||
|
this.billingRepository.checkoutSessions.upsertFromStripe(cs, {eventCreated: event.created}),
|
||||||
|
);
|
||||||
await this.checkoutHandler.handleAsyncPaymentFailed(cs);
|
await this.checkoutHandler.handleAsyncPaymentFailed(cs);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'invoice.paid':
|
case 'invoice.paid':
|
||||||
case 'invoice.payment_succeeded': {
|
case 'invoice.payment_succeeded': {
|
||||||
const inv = event.data.object as Stripe.Invoice;
|
const inv = event.data.object as Stripe.Invoice;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(inv));
|
await this.mirrorInvoice(event, inv);
|
||||||
await this.subscriptionHandler.handleInvoicePaymentSucceeded(event.id, inv);
|
await this.subscriptionHandler.handleInvoicePaymentSucceeded(event.id, inv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'invoice.payment_failed': {
|
case 'invoice.payment_failed': {
|
||||||
const inv = event.data.object as Stripe.Invoice;
|
const inv = event.data.object as Stripe.Invoice;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(inv));
|
await this.mirrorInvoice(event, inv);
|
||||||
await this.subscriptionHandler.handleInvoicePaymentFailed(inv);
|
await this.subscriptionHandler.handleInvoicePaymentFailed(inv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'invoice.payment_action_required': {
|
case 'invoice.payment_action_required': {
|
||||||
const inv = event.data.object as Stripe.Invoice;
|
const inv = event.data.object as Stripe.Invoice;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(inv));
|
await this.mirrorInvoice(event, inv);
|
||||||
await this.subscriptionHandler.handleInvoicePaymentActionRequired(inv);
|
await this.subscriptionHandler.handleInvoicePaymentActionRequired(inv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'invoice.finalization_failed': {
|
case 'invoice.finalization_failed': {
|
||||||
const inv = event.data.object as Stripe.Invoice;
|
const inv = event.data.object as Stripe.Invoice;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(inv));
|
await this.mirrorInvoice(event, inv);
|
||||||
await this.subscriptionHandler.handleInvoiceFinalizationFailed(inv);
|
await this.subscriptionHandler.handleInvoiceFinalizationFailed(inv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'invoice.updated': {
|
case 'invoice.updated': {
|
||||||
const inv = event.data.object as Stripe.Invoice;
|
const inv = event.data.object as Stripe.Invoice;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(inv));
|
await this.mirrorInvoice(event, inv);
|
||||||
await this.subscriptionHandler.handleInvoiceUpdated(inv);
|
await this.subscriptionHandler.handleInvoiceUpdated(inv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -253,11 +257,12 @@ export class StripeWebhookService {
|
|||||||
case 'charge.refunded': {
|
case 'charge.refunded': {
|
||||||
const c = event.data.object as Stripe.Charge;
|
const c = event.data.object as Stripe.Charge;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.charges.upsertFromStripe(c));
|
await this.safeMirrorUpsert(event, () => this.billingRepository.charges.upsertFromStripe(c));
|
||||||
const refunds = c.refunds?.data ?? [];
|
const refunds = await this.listChargeRefunds(c);
|
||||||
for (const r of refunds) {
|
for (const r of refunds) {
|
||||||
await this.safeMirrorUpsert(event, () =>
|
await this.safeMirrorUpsert(event, () =>
|
||||||
this.billingRepository.refunds.upsertFromStripe(r, {
|
this.billingRepository.refunds.upsertFromStripe(r, {
|
||||||
customerId: typeof c.customer === 'string' ? c.customer : (c.customer?.id ?? undefined),
|
customerId: typeof c.customer === 'string' ? c.customer : (c.customer?.id ?? undefined),
|
||||||
|
livemode: event.livemode,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -324,7 +329,13 @@ export class StripeWebhookService {
|
|||||||
case 'refund.updated':
|
case 'refund.updated':
|
||||||
case 'refund.failed': {
|
case 'refund.failed': {
|
||||||
const r = event.data.object as Stripe.Refund;
|
const r = event.data.object as Stripe.Refund;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.refunds.upsertFromStripe(r));
|
const customerId = await this.resolveRefundCustomerId(r);
|
||||||
|
await this.safeMirrorUpsert(event, () =>
|
||||||
|
this.billingRepository.refunds.upsertFromStripe(r, {
|
||||||
|
customerId: customerId ?? undefined,
|
||||||
|
livemode: event.livemode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'invoice.created':
|
case 'invoice.created':
|
||||||
@@ -332,12 +343,14 @@ export class StripeWebhookService {
|
|||||||
case 'invoice.voided':
|
case 'invoice.voided':
|
||||||
case 'invoice.marked_uncollectible': {
|
case 'invoice.marked_uncollectible': {
|
||||||
const inv = event.data.object as Stripe.Invoice;
|
const inv = event.data.object as Stripe.Invoice;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(inv));
|
await this.mirrorInvoice(event, inv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'checkout.session.expired': {
|
case 'checkout.session.expired': {
|
||||||
const cs = event.data.object as Stripe.Checkout.Session;
|
const cs = event.data.object as Stripe.Checkout.Session;
|
||||||
await this.safeMirrorUpsert(event, () => this.billingRepository.checkoutSessions.upsertFromStripe(cs));
|
await this.safeMirrorUpsert(event, () =>
|
||||||
|
this.billingRepository.checkoutSessions.upsertFromStripe(cs, {eventCreated: event.created}),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'charge.dispute.updated':
|
case 'charge.dispute.updated':
|
||||||
@@ -353,6 +366,54 @@ export class StripeWebhookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveRefundCustomerId(refund: Stripe.Refund): Promise<string | null> {
|
||||||
|
const chargeId = typeof refund.charge === 'string' ? refund.charge : (refund.charge?.id ?? null);
|
||||||
|
if (chargeId !== null) {
|
||||||
|
const chargeRow = await this.billingRepository.charges.findById(chargeId);
|
||||||
|
if (chargeRow?.customer_id != null) {
|
||||||
|
return chargeRow.customer_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const paymentIntentId =
|
||||||
|
typeof refund.payment_intent === 'string' ? refund.payment_intent : (refund.payment_intent?.id ?? null);
|
||||||
|
if (paymentIntentId !== null) {
|
||||||
|
const paymentIntentRow = await this.billingRepository.paymentIntents.findById(paymentIntentId);
|
||||||
|
if (paymentIntentRow?.customer_id != null) {
|
||||||
|
return paymentIntentRow.customer_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listChargeRefunds(charge: Stripe.Charge): Promise<Array<Stripe.Refund>> {
|
||||||
|
const inlined = charge.refunds?.data ?? [];
|
||||||
|
if (inlined.length > 0 || charge.id == null || this.stripe == null) {
|
||||||
|
return inlined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const listed = await this.stripe.refunds.list({charge: charge.id, limit: 100});
|
||||||
|
return listed.data;
|
||||||
|
} catch (listErr) {
|
||||||
|
Logger.warn({listErr, chargeId: charge.id}, 'Failed to list refunds for charge; skipping refund mirror');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async mirrorInvoice(event: Stripe.Event, inv: Stripe.Invoice): Promise<void> {
|
||||||
|
let hydrated = inv;
|
||||||
|
if (inv.payments === undefined && inv.id != null && this.stripe != null) {
|
||||||
|
try {
|
||||||
|
hydrated = await this.stripe.invoices.retrieve(inv.id, {expand: ['payments.data.payment']});
|
||||||
|
} catch (hydrateErr) {
|
||||||
|
Logger.warn(
|
||||||
|
{hydrateErr, eventId: event.id, invoiceId: inv.id},
|
||||||
|
'Failed to hydrate invoice payments; mirroring invoice without payment rows',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.safeMirrorUpsert(event, () => this.billingRepository.invoices.upsertFromStripe(hydrated));
|
||||||
|
}
|
||||||
|
|
||||||
private async safeMirrorUpsert(event: Stripe.Event, fn: () => Promise<unknown>): Promise<void> {
|
private async safeMirrorUpsert(event: Stripe.Event, fn: () => Promise<unknown>): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fn();
|
await fn();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface MockStripeInvoice {
|
|||||||
id: string;
|
id: string;
|
||||||
object: 'invoice';
|
object: 'invoice';
|
||||||
customer: string;
|
customer: string;
|
||||||
subscription: string | null;
|
parent: {subscription_details: {subscription: string}} | null;
|
||||||
amount_due: number;
|
amount_due: number;
|
||||||
amount_paid: number;
|
amount_paid: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
@@ -66,7 +66,7 @@ function buildInvoice(opts: {
|
|||||||
id: opts.id,
|
id: opts.id,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
customer: customerId,
|
customer: customerId,
|
||||||
subscription: subscriptionId,
|
parent: subscriptionId == null ? null : {subscription_details: {subscription: subscriptionId}},
|
||||||
amount_due: 2500,
|
amount_due: 2500,
|
||||||
amount_paid: 2500,
|
amount_paid: 2500,
|
||||||
currency: 'usd',
|
currency: 'usd',
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -217,7 +217,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -252,7 +252,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: invoiceId,
|
id: invoiceId,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -300,7 +300,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_create',
|
billing_reason: 'subscription_create',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -351,7 +351,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -372,7 +372,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -422,7 +422,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_cycle',
|
billing_reason: 'subscription_cycle',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -460,7 +460,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_update',
|
billing_reason: 'subscription_update',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
amount_paid: 0,
|
amount_paid: 0,
|
||||||
amount_due: 0,
|
amount_due: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -503,7 +503,7 @@ describe('Stripe Webhook - Invoice Events', () => {
|
|||||||
object: {
|
object: {
|
||||||
id: `in_test_${Date.now()}`,
|
id: `in_test_${Date.now()}`,
|
||||||
billing_reason: 'subscription_update',
|
billing_reason: 'subscription_update',
|
||||||
subscription: subscriptionId,
|
parent: {subscription_details: {subscription: subscriptionId}},
|
||||||
amount_paid: 1250,
|
amount_paid: 1250,
|
||||||
amount_due: 1250,
|
amount_due: 1250,
|
||||||
total: 1250,
|
total: 1250,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ interface StripeApiMockConfig {
|
|||||||
subscriptionsListEmpty?: boolean;
|
subscriptionsListEmpty?: boolean;
|
||||||
charges?: Record<string, Partial<MockStripeCharge>>;
|
charges?: Record<string, Partial<MockStripeCharge>>;
|
||||||
customers?: Record<string, Partial<MockStripeCustomer>>;
|
customers?: Record<string, Partial<MockStripeCustomer>>;
|
||||||
invoices?: Record<string, Partial<MockStripeInvoice>>;
|
invoices?: Record<string, Partial<MockStripeInvoice> & {subscriptionId?: string}>;
|
||||||
paymentIntents?: Record<string, Partial<MockStripePaymentIntent>>;
|
paymentIntents?: Record<string, Partial<MockStripePaymentIntent>>;
|
||||||
paymentMethods?: Record<string, Partial<MockStripePaymentMethod>>;
|
paymentMethods?: Record<string, Partial<MockStripePaymentMethod>>;
|
||||||
setupIntents?: Record<string, Partial<MockStripeSetupIntent>>;
|
setupIntents?: Record<string, Partial<MockStripeSetupIntent>>;
|
||||||
@@ -284,7 +284,10 @@ interface MockStripeInvoice {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
status: 'draft' | 'open' | 'paid' | 'uncollectible' | 'void' | null;
|
status: 'draft' | 'open' | 'paid' | 'uncollectible' | 'void' | null;
|
||||||
subscription: string | null;
|
parent: {
|
||||||
|
type: 'subscription_details';
|
||||||
|
subscription_details: {subscription: string};
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MockStripeValueList {
|
interface MockStripeValueList {
|
||||||
@@ -571,13 +574,14 @@ export function createStripeApiHandlers(config: StripeApiMockConfig = {}): Strip
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const [invoiceId, overrides] of Object.entries(config.invoices ?? {})) {
|
for (const [invoiceId, overrides] of Object.entries(config.invoices ?? {})) {
|
||||||
|
const {subscriptionId, ...invoiceOverrides} = overrides;
|
||||||
const defaultInvoice = createDefaultInvoice(invoiceId, {
|
const defaultInvoice = createDefaultInvoice(invoiceId, {
|
||||||
customerId: overrides.customer ?? 'cus_test_1',
|
customerId: overrides.customer ?? 'cus_test_1',
|
||||||
subscriptionId: overrides.subscription ?? 'sub_test_1',
|
subscriptionId: subscriptionId ?? 'sub_test_1',
|
||||||
});
|
});
|
||||||
invoiceStore.set(invoiceId, {
|
invoiceStore.set(invoiceId, {
|
||||||
...defaultInvoice,
|
...defaultInvoice,
|
||||||
...overrides,
|
...invoiceOverrides,
|
||||||
id: invoiceId,
|
id: invoiceId,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
payments: overrides.payments ?? defaultInvoice.payments,
|
payments: overrides.payments ?? defaultInvoice.payments,
|
||||||
@@ -739,7 +743,10 @@ export function createStripeApiHandlers(config: StripeApiMockConfig = {}): Strip
|
|||||||
url: `/v1/invoices/${id}/payments`,
|
url: `/v1/invoices/${id}/payments`,
|
||||||
},
|
},
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
subscription: subscriptionId,
|
parent:
|
||||||
|
subscriptionId === null
|
||||||
|
? null
|
||||||
|
: {type: 'subscription_details', subscription_details: {subscription: subscriptionId}},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getPaymentIntent(paymentIntentId: string): MockStripePaymentIntent {
|
function getPaymentIntent(paymentIntentId: string): MockStripePaymentIntent {
|
||||||
@@ -838,8 +845,6 @@ export function createStripeApiHandlers(config: StripeApiMockConfig = {}): Strip
|
|||||||
object: 'subscription',
|
object: 'subscription',
|
||||||
customer: subState.customer,
|
customer: subState.customer,
|
||||||
status: subState.status,
|
status: subState.status,
|
||||||
current_period_start: subState.current_period_start,
|
|
||||||
current_period_end: subState.current_period_end,
|
|
||||||
latest_invoice: subState.latest_invoice ? getInvoice(subState.latest_invoice) : null,
|
latest_invoice: subState.latest_invoice ? getInvoice(subState.latest_invoice) : null,
|
||||||
trial_end: subState.trial_end,
|
trial_end: subState.trial_end,
|
||||||
items: {
|
items: {
|
||||||
@@ -1070,7 +1075,7 @@ export function createStripeApiHandlers(config: StripeApiMockConfig = {}): Strip
|
|||||||
const limit = Number.parseInt(requestUrl.searchParams.get('limit') ?? '10', 10);
|
const limit = Number.parseInt(requestUrl.searchParams.get('limit') ?? '10', 10);
|
||||||
const sortedInvoices = [...invoiceStore.values()]
|
const sortedInvoices = [...invoiceStore.values()]
|
||||||
.filter((invoice) => !customerId || invoice.customer === customerId)
|
.filter((invoice) => !customerId || invoice.customer === customerId)
|
||||||
.filter((invoice) => !subscriptionId || invoice.subscription === subscriptionId)
|
.filter((invoice) => !subscriptionId || invoice.parent?.subscription_details?.subscription === subscriptionId)
|
||||||
.sort((left, right) => right.created - left.created);
|
.sort((left, right) => right.created - left.created);
|
||||||
const startIndex = startingAfter ? sortedInvoices.findIndex((invoice) => invoice.id === startingAfter) + 1 : 0;
|
const startIndex = startingAfter ? sortedInvoices.findIndex((invoice) => invoice.id === startingAfter) + 1 : 0;
|
||||||
const pagedInvoices = sortedInvoices.slice(Math.max(startIndex, 0), Math.max(startIndex, 0) + limit);
|
const pagedInvoices = sortedInvoices.slice(Math.max(startIndex, 0), Math.max(startIndex, 0) + limit);
|
||||||
@@ -1460,7 +1465,6 @@ export function createStripeApiHandlers(config: StripeApiMockConfig = {}): Strip
|
|||||||
object: 'subscription',
|
object: 'subscription',
|
||||||
customer,
|
customer,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
current_period_start: Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60,
|
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
items: {
|
items: {
|
||||||
object: 'list',
|
object: 'list',
|
||||||
@@ -1482,6 +1486,7 @@ export function createStripeApiHandlers(config: StripeApiMockConfig = {}): Strip
|
|||||||
livemode: false,
|
livemode: false,
|
||||||
},
|
},
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
current_period_start: Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60,
|
||||||
current_period_end: currentPeriodEnd,
|
current_period_end: currentPeriodEnd,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1879,11 +1884,13 @@ export function createInvoicePaidEvent(options: {
|
|||||||
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
customer: options.customerId ?? 'cus_test_1',
|
customer: options.customerId ?? 'cus_test_1',
|
||||||
subscription: options.subscriptionId ?? 'sub_test_1',
|
parent: {
|
||||||
|
type: 'subscription_details',
|
||||||
|
subscription_details: {subscription: options.subscriptionId ?? 'sub_test_1'},
|
||||||
|
},
|
||||||
amount_paid: options.amountPaid ?? 2500,
|
amount_paid: options.amountPaid ?? 2500,
|
||||||
currency: options.currency ?? 'usd',
|
currency: options.currency ?? 'usd',
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
paid: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1902,7 +1909,10 @@ export function createInvoicePaymentFailedEvent(options: {
|
|||||||
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
customer: options.customerId ?? 'cus_test_1',
|
customer: options.customerId ?? 'cus_test_1',
|
||||||
subscription: options.subscriptionId ?? 'sub_test_1',
|
parent: {
|
||||||
|
type: 'subscription_details',
|
||||||
|
subscription_details: {subscription: options.subscriptionId ?? 'sub_test_1'},
|
||||||
|
},
|
||||||
amount_due: options.amountDue ?? 2500,
|
amount_due: options.amountDue ?? 2500,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
paid: false,
|
paid: false,
|
||||||
@@ -1926,7 +1936,10 @@ export function createInvoicePaymentActionRequiredEvent(options: {
|
|||||||
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
customer: options.customerId ?? 'cus_test_1',
|
customer: options.customerId ?? 'cus_test_1',
|
||||||
subscription: options.subscriptionId ?? 'sub_test_1',
|
parent: {
|
||||||
|
type: 'subscription_details',
|
||||||
|
subscription_details: {subscription: options.subscriptionId ?? 'sub_test_1'},
|
||||||
|
},
|
||||||
amount_due: options.amountDue ?? 2500,
|
amount_due: options.amountDue ?? 2500,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
paid: false,
|
paid: false,
|
||||||
@@ -1950,7 +1963,10 @@ export function createInvoiceFinalizationFailedEvent(options: {
|
|||||||
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
customer: options.customerId ?? 'cus_test_1',
|
customer: options.customerId ?? 'cus_test_1',
|
||||||
subscription: options.subscriptionId ?? 'sub_test_1',
|
parent: {
|
||||||
|
type: 'subscription_details',
|
||||||
|
subscription_details: {subscription: options.subscriptionId ?? 'sub_test_1'},
|
||||||
|
},
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
paid: false,
|
paid: false,
|
||||||
last_finalization_error: {
|
last_finalization_error: {
|
||||||
@@ -1980,7 +1996,10 @@ export function createInvoiceUpdatedEvent(options: {
|
|||||||
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
id: options.invoiceId ?? `in_test_${Date.now()}`,
|
||||||
object: 'invoice',
|
object: 'invoice',
|
||||||
customer: options.customerId ?? 'cus_test_1',
|
customer: options.customerId ?? 'cus_test_1',
|
||||||
subscription: options.subscriptionId ?? 'sub_test_1',
|
parent: {
|
||||||
|
type: 'subscription_details',
|
||||||
|
subscription_details: {subscription: options.subscriptionId ?? 'sub_test_1'},
|
||||||
|
},
|
||||||
amount_due: options.amountDue ?? 2500,
|
amount_due: options.amountDue ?? 2500,
|
||||||
status: options.status ?? 'open',
|
status: options.status ?? 'open',
|
||||||
paid: options.paid ?? false,
|
paid: options.paid ?? false,
|
||||||
|
|||||||
Reference in new issue
Block a user