This repository has no description
Something went wrong. Try again.
57 kB · 1471 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472// SPDX-License-Identifier: AGPL-3.0-or-later
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';import {isEuEeaCountryCode} from '@fluxer/constants/src/EuropeanEconomicArea';import {FeatureNotAvailableSelfHostedError} from '@fluxer/errors/src/domains/core/FeatureNotAvailableSelfHostedError';import {NotFoundError} from '@fluxer/errors/src/domains/core/NotFoundError';import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError';import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError';import {StripeNoActiveSubscriptionError} from '@fluxer/errors/src/domains/payment/StripeNoActiveSubscriptionError';import {StripePaymentNotAvailableError} from '@fluxer/errors/src/domains/payment/StripePaymentNotAvailableError';import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';import { AdminBillingCancelImmediatelyRequest, AdminBillingOverviewResponse, AdminBillingRefundLatestInvoiceCancelRequest, AdminBillingRefundLatestInvoiceCancelResponse, AdminBillingRefundRequest, AdminInvoiceListResponse, AdminPaymentListResponse, AdminPaymentMethodListResponse, AdminSubscriptionResponse,} from '@fluxer/schema/src/domains/admin/AdminBillingSchemas';import type Stripe from 'stripe';import {createUserID, type UserID} from '../../BrandedTypes';import type {BillingRepository} from '../../billing/repositories/BillingRepository';import {Config} from '../../Config';import type { BillingChargeRow, BillingCheckoutSessionRow, BillingInvoiceRow, BillingPaymentIntentRow, BillingPaymentMethodRow, BillingPaymentRow, BillingPriceRow, BillingRefundRow, BillingSubscriptionRow,} from '../../database/types/BillingTypes';import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService';import {Logger} from '../../Logger';import {requireAdminACL} from '../../middleware/AdminMiddleware';import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';import {getBillingRepository} from '../../middleware/ServiceRegistry';import type {Payment} from '../../models/Payment';import type {User} from '../../models/User';import type {StripeService} from '../../stripe/StripeService';import type {HonoApp, HonoEnv} from '../../types/HonoEnv';import type {IUserRepository} from '../../user/IUserRepository';import {PaymentRepository} from '../../user/repositories/PaymentRepository';import {Validator} from '../../Validator';import {AdminRepository} from '../AdminRepository';import {AdminAuditService} from '../services/AdminAuditService';
function ensureBillingFeatureAvailable(): void { if (Config.instance.selfHosted) { throw new FeatureNotAvailableSelfHostedError(); }}
async function getRequiredUser(userRepository: IUserRepository, userId: UserID): Promise<User> { const user = await userRepository.findUnique(userId); if (!user) { throw new UnknownUserError(); } return user;}
function buildPaymentNotFoundError(): NotFoundError { return new NotFoundError({ code: APIErrorCodes.NOT_FOUND, });}
interface AdminRefundPaymentIntentLookup { findById(paymentIntentId: string): Promise<BillingPaymentIntentRow | null>;}
interface AdminRefundBillingLookup { paymentIntents: AdminRefundPaymentIntentLookup;}
interface MirrorBillingPaymentRecord { charge: BillingChargeRow | null; checkoutSession: BillingCheckoutSessionRow | null; invoice: BillingInvoiceRow; primaryPayment: BillingPaymentRow | null; localPayment: Payment | null; refunds: Array<BillingRefundRow>;}
type AdminImmediateCancelRefundPolicy = 'full_refund' | 'prorated_refund' | 'cancel_only';type AdminImmediateCancelRefundPolicyBasis = 'support_policy' | 'eu_eea_withdrawal_no_waiver';
interface AdminImmediateCancelRefundTarget { amountPaidCents: number; chargeId: string | null; currency: string; invoiceId: string; invoiceCreatedAt: Date; paymentCompletedAt: Date | null; paymentIntentId: string | null; paidAt: Date | null; purchaseGeoipCountryCode: string | null; purchaseClientCountryCode: string | null; euWithdrawalWaiverRequired: boolean; euWithdrawalWaiverAccepted: boolean; euWithdrawalWaiverAcceptedAt: Date | null; euWithdrawalWaiverTextVersion: string | null; stripeBillingCountryCode: string | null; stripeCustomerCountryCode: string | null; stripePaymentMethodCountryCode: string | null; stripeTermsOfServiceAccepted: boolean | null;}
interface AdminImmediateCancelRefundDecision { amountCents: number | null; basis: AdminImmediateCancelRefundPolicyBasis; cycleElapsedDays: number; policy: AdminImmediateCancelRefundPolicy;}
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;const EU_EEA_WITHDRAWAL_REFUND_WINDOW_DAYS = 14;
function getElapsedDays(from: Date, now: Date): number { return Math.max(0, Math.floor((now.getTime() - from.getTime()) / MILLISECONDS_PER_DAY));}
function normalizeCountryCode(value: string | null | undefined): string | null { const normalized = value?.trim().toUpperCase(); return normalized && /^[A-Z]{2}$/.test(normalized) ? normalized : null;}
function parseStripeMetadataBoolean(value: string | null | undefined): boolean | null { if (value === 'true') return true; if (value === 'false') return false; return null;}
function getCheckoutSessionAcceptedAt(session: BillingCheckoutSessionRow | null): Date | null { const acceptedAt = session?.metadata?.get('eu_withdrawal_waiver_accepted_at'); if (!acceptedAt) return null; const date = new Date(acceptedAt); return Number.isNaN(date.getTime()) ? null : date;}
function getCheckoutSessionMetadata(session: BillingCheckoutSessionRow | null): Map<string, string> { return session?.metadata ?? new Map();}
function getMetadataString(map: Map<string, string>, key: string): string | null { return map.get(key) ?? null;}
function isManageableSubscriptionRow(row: BillingSubscriptionRow | null | undefined): row is BillingSubscriptionRow { return Boolean(row && row.status !== 'canceled' && row.status !== 'incomplete_expired');}
function getSubscriptionStatusPriority(status: BillingSubscriptionRow['status']): number { switch (status) { case 'active': return 0; case 'trialing': return 1; case 'past_due': return 2; case 'unpaid': return 3; case 'paused': return 4; case 'incomplete': return 5; case 'canceled': return 6; case 'incomplete_expired': return 7; default: return 8; }}
function getSubscriptionSortTimestamp(sub: BillingSubscriptionRow): number { const ts = sub.current_period_end ?? sub.canceled_at ?? sub.cancel_at ?? sub.started_at ?? sub.stripe_created_at; return ts ? ts.getTime() : 0;}
function sortSubscriptionsByRelevance(subs: Array<BillingSubscriptionRow>): Array<BillingSubscriptionRow> { return [...subs].sort((left, right) => { const statusDiff = getSubscriptionStatusPriority(left.status) - getSubscriptionStatusPriority(right.status); if (statusDiff !== 0) return statusDiff; return getSubscriptionSortTimestamp(right) - getSubscriptionSortTimestamp(left); });}
function getRefundAmount(refunds: Array<BillingRefundRow>): number { return refunds .filter((refund) => refund.status !== 'failed' && refund.status !== 'canceled') .reduce((total, refund) => total + Number(refund.amount ?? 0n), 0);}
function buildPaymentStatus( invoice: BillingInvoiceRow, payment: BillingPaymentRow | null, refunds: Array<BillingRefundRow>,): string { const refundedAmount = getRefundAmount(refunds); const amountPaid = Number(invoice.amount_paid ?? 0n); if (amountPaid > 0 && refundedAmount >= amountPaid) { return 'refunded'; } if (refundedAmount > 0) { return 'partially_refunded'; } return payment?.status ?? invoice.status ?? 'unknown';}
function decideImmediateCancelRefund(params: { amountPaidCents: number; now: Date; refundTarget: AdminImmediateCancelRefundTarget | null; subscription: BillingSubscriptionRow;}): AdminImmediateCancelRefundDecision { const startMs = params.subscription.current_period_start?.getTime() ?? null; const endMs = params.subscription.current_period_end?.getTime() ?? null; const cycle = startMs && endMs && endMs > startMs ? {start: startMs, end: endMs} : null; const cycleElapsedDays = cycle ? getElapsedDays(new Date(cycle.start), params.now) : 0; const purchaseDate = params.refundTarget?.paymentCompletedAt ?? params.refundTarget?.paidAt ?? params.refundTarget?.invoiceCreatedAt ?? null; if ( params.refundTarget?.euWithdrawalWaiverRequired && !params.refundTarget.euWithdrawalWaiverAccepted && purchaseDate && getElapsedDays(purchaseDate, params.now) <= EU_EEA_WITHDRAWAL_REFUND_WINDOW_DAYS ) { return { amountCents: params.amountPaidCents, basis: 'eu_eea_withdrawal_no_waiver', cycleElapsedDays, policy: 'full_refund', }; } if (!cycle) { return { amountCents: params.amountPaidCents, basis: 'support_policy', cycleElapsedDays, policy: 'full_refund', }; } const nowMs = params.now.getTime(); const elapsedDays = cycleElapsedDays; if (elapsedDays <= 4) { return { amountCents: params.amountPaidCents, basis: 'support_policy', cycleElapsedDays: elapsedDays, policy: 'full_refund', }; } if (elapsedDays <= 18) { const totalMs = Math.max(1, cycle.end - cycle.start); const remainingMs = Math.max(0, cycle.end - nowMs); const proratedAmount = Math.max( 1, Math.min(params.amountPaidCents, Math.ceil(params.amountPaidCents * (remainingMs / totalMs))), ); return { amountCents: proratedAmount, basis: 'support_policy', cycleElapsedDays: elapsedDays, policy: 'prorated_refund', }; } return { amountCents: null, basis: 'support_policy', cycleElapsedDays: elapsedDays, policy: 'cancel_only', };}
async function assertOwnedPaymentIntentForAdminRefund( userRepository: Pick<IUserRepository, 'getPaymentByPaymentIntent'>, billingRepository: AdminRefundBillingLookup, targetUser: Pick<User, 'id' | 'stripeCustomerId'>, paymentIntentId: string,): Promise<void> { const payment = await userRepository.getPaymentByPaymentIntent(paymentIntentId); if (payment) { if (payment.userId === targetUser.id) { return; } throw buildPaymentNotFoundError(); } if (!targetUser.stripeCustomerId) { throw buildPaymentNotFoundError(); } const mirroredIntent = await billingRepository.paymentIntents.findById(paymentIntentId); if (mirroredIntent && mirroredIntent.customer_id === targetUser.stripeCustomerId) { return; } throw buildPaymentNotFoundError();}
class BillingAdminControllerService { private readonly paymentRepository = new PaymentRepository();
constructor( private readonly userRepository: IUserRepository, private readonly stripeService: StripeService | null, private readonly auditService: AdminAuditService, private readonly billingRepository: BillingRepository, ) {}
private get stripe(): Stripe | null { return this.stripeService?.getStripe() ?? null; }
private async resolveCustomerIds(user: User): Promise<Array<string>> { const ids = new Set<string>(); if (user.stripeCustomerId) { ids.add(user.stripeCustomerId); } const mirroredCustomers = await this.billingRepository.customers.findByUserId(user.id); for (const c of mirroredCustomers) { if (!c.deleted) ids.add(c.provider_id); } return [...ids]; }
async getResolvedStripeCustomerId(user: User): Promise<string | null> { const subscription = await this.resolvePrimaryStripeSubscription(user); if (subscription?.customer_id) return subscription.customer_id; const ids = await this.resolveCustomerIds(user); return ids[0] ?? user.stripeCustomerId ?? null; }
private async resolveStripeSubscriptions(user: User): Promise<Array<BillingSubscriptionRow>> { const seen = new Set<string>(); const out: Array<BillingSubscriptionRow> = []; const add = (row: BillingSubscriptionRow | null) => { if (row && !seen.has(row.provider_id)) { seen.add(row.provider_id); out.push(row); } }; if (user.stripeSubscriptionId) { add(await this.billingRepository.subscriptions.findById(user.stripeSubscriptionId)); } const userSubs = await this.billingRepository.subscriptions.listByUser(user.id); for (const ref of userSubs) { if (seen.has(ref.provider_id)) continue; add(await this.billingRepository.subscriptions.findById(ref.provider_id)); } const customerIds = await this.resolveCustomerIds(user); for (const customerId of customerIds) { const refs = await this.billingRepository.subscriptions.listByCustomer(customerId); for (const ref of refs) { if (seen.has(ref.provider_id)) continue; add(await this.billingRepository.subscriptions.findById(ref.provider_id)); } } return sortSubscriptionsByRelevance(out); }
private async resolvePrimaryStripeSubscription(user: User): Promise<BillingSubscriptionRow | null> { return (await this.resolveStripeSubscriptions(user))[0] ?? null; }
private async resolveManageableStripeSubscription(user: User): Promise<BillingSubscriptionRow | null> { return (await this.resolveStripeSubscriptions(user)).find(isManageableSubscriptionRow) ?? null; }
private async syncResolvedStripeState( user: User, params: { customerId?: string | null; subscriptionId?: string | null; premiumWillCancel?: boolean; }, ): Promise<User> { const patch: { premium_will_cancel?: boolean; stripe_customer_id?: string | null; stripe_subscription_id?: string | null; } = {}; if (params.customerId !== undefined && params.customerId !== user.stripeCustomerId) { patch.stripe_customer_id = params.customerId; } if (params.subscriptionId !== undefined && params.subscriptionId !== user.stripeSubscriptionId) { patch.stripe_subscription_id = params.subscriptionId; } if (params.premiumWillCancel !== undefined && params.premiumWillCancel !== user.premiumWillCancel) { patch.premium_will_cancel = params.premiumWillCancel; } if (Object.keys(patch).length === 0) { return user; } return this.userRepository.patchUpsert(user.id, patch, user.toRow()); }
async getUserPayments(user: User): Promise<Array<Payment>> { const payments = await this.paymentRepository.findPaymentsByUserId(user.id); return payments.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime()); }
async getUserSubscription(user: User): Promise<BillingSubscriptionRow | null> { return this.resolvePrimaryStripeSubscription(user); }
async getUserPaymentMethods(user: User): Promise<Array<BillingPaymentMethodRow>> { const customerIds = await this.resolveCustomerIds(user); if (customerIds.length === 0) return []; const byId = new Map<string, BillingPaymentMethodRow>(); for (const customerId of customerIds) { const rows = await this.billingRepository.paymentMethods.listByCustomer(customerId); for (const row of rows) { byId.set(row.provider_id, row); } } return [...byId.values()].sort((left, right) => { const lt = left.stripe_created_at?.getTime() ?? 0; const rt = right.stripe_created_at?.getTime() ?? 0; return rt - lt; }); }
async getUserInvoices( user: User, limit: number = 25, ): Promise<{ invoices: Array<BillingInvoiceRow>; has_more: boolean; }> { const customerIds = await this.resolveCustomerIds(user); if (customerIds.length === 0) return {invoices: [], has_more: false}; const aggregated = new Map<string, BillingInvoiceRow>(); let hasMore = false; for (const customerId of customerIds) { const result = await this.billingRepository.invoices.listByCustomer(customerId, {pageSize: 100}); for (const inv of result.rows) { aggregated.set(inv.provider_id, inv); } if (result.pageState) hasMore = true; } const merged = [...aggregated.values()].sort((left, right) => { const lt = left.stripe_created_at?.getTime() ?? 0; const rt = right.stripe_created_at?.getTime() ?? 0; return rt - lt; }); return { invoices: merged.slice(0, limit), has_more: hasMore || merged.length > limit, }; }
private async getRefundsForInvoiceAndCharge(params: { invoiceId: string; chargeId: string | null; paymentIntentId: string | null; }): Promise<Array<BillingRefundRow>> { const byId = new Map<string, BillingRefundRow>(); const add = (rows: Array<BillingRefundRow>) => { for (const row of rows) byId.set(row.provider_id, row); }; add(await this.billingRepository.refunds.listByInvoice(params.invoiceId)); if (params.chargeId) { add(await this.billingRepository.refunds.listByCharge(params.chargeId)); } if (params.paymentIntentId) { add(await this.billingRepository.refunds.listByPaymentIntent(params.paymentIntentId)); } return [...byId.values()]; }
private async getCheckoutSessionForInvoice(params: { paymentIntentId: string | null; subscriptionId: string | null; customerIds: Array<string>; }): Promise<BillingCheckoutSessionRow | null> { for (const customerId of params.customerIds) { const result = await this.billingRepository.checkoutSessions.listByCustomer(customerId, {pageSize: 100}); for (const ref of result.rows) { const session = await this.billingRepository.checkoutSessions.findById(ref.provider_id); if (!session) continue; if (params.paymentIntentId && session.payment_intent_id === params.paymentIntentId) { return session; } if (params.subscriptionId && session.subscription_id === params.subscriptionId) { return session; } } } return null; }
async getMirrorBillingPaymentRecords(user: User, limit: number = 25): Promise<Array<MirrorBillingPaymentRecord>> { const {invoices} = await this.getUserInvoices(user, limit); const customerIds = await this.resolveCustomerIds(user); const localPayments = await this.getUserPayments(user); const localPaymentsByInvoiceId = new Map<string, Payment>(); for (const lp of localPayments) { if (lp.invoiceId && !localPaymentsByInvoiceId.has(lp.invoiceId)) { localPaymentsByInvoiceId.set(lp.invoiceId, lp); } } const records = await Promise.all( invoices.map(async (invoice): Promise<MirrorBillingPaymentRecord> => { const primaryPayment = await this.billingRepository.payments.findPrimaryForInvoice(invoice.provider_id); const chargeId = primaryPayment?.charge_id ?? null; const paymentIntentId = primaryPayment?.payment_intent_id ?? null; const charge = chargeId ? await this.billingRepository.charges.findById(chargeId) : null; const refunds = await this.getRefundsForInvoiceAndCharge({ invoiceId: invoice.provider_id, chargeId, paymentIntentId, }); const checkoutSession = await this.getCheckoutSessionForInvoice({ paymentIntentId, subscriptionId: invoice.subscription_id, customerIds, }); return { charge, checkoutSession, invoice, primaryPayment, localPayment: invoice.provider_id ? (localPaymentsByInvoiceId.get(invoice.provider_id) ?? null) : null, refunds, }; }), ); return records.filter((record) => { const amountPaid = Number(record.invoice.amount_paid ?? 0n); return amountPaid > 0 || record.refunds.length > 0 || record.primaryPayment; }); }
private buildRefundTargetFromMirror( invoice: BillingInvoiceRow, primaryPayment: BillingPaymentRow | null, charge: BillingChargeRow | null, checkoutSession: BillingCheckoutSessionRow | null, expectedCustomerId: string | null, localPayment: Payment | null, ): AdminImmediateCancelRefundTarget | null { if (!invoice.provider_id || invoice.status !== 'paid') return null; const amountPaid = Number(invoice.amount_paid ?? 0n); if (amountPaid <= 0) return null; if (expectedCustomerId && invoice.customer_id && invoice.customer_id !== expectedCustomerId) return null; if (!primaryPayment?.payment_intent_id && !primaryPayment?.charge_id) return null; const sessionMetadata = getCheckoutSessionMetadata(checkoutSession); const stripePaymentMethodCountryCode = normalizeCountryCode(charge?.card_country ?? null); const stripeBillingCountryCode: string | null = null; const stripeCustomerCountryCode: string | null = null; const stripeTermsOfServiceAccepted: boolean | null = null; const metadataWaiverRequired = parseStripeMetadataBoolean( getMetadataString(sessionMetadata, 'eu_withdrawal_waiver_required'), ); const metadataWaiverAccepted = parseStripeMetadataBoolean( getMetadataString(sessionMetadata, 'eu_withdrawal_waiver_accepted'), ); const stripeCountryRequiresWaiver = [ stripeBillingCountryCode, stripeCustomerCountryCode, stripePaymentMethodCountryCode, ].some((countryCode) => isEuEeaCountryCode(countryCode)); const euWithdrawalWaiverRequired = (localPayment?.euWithdrawalWaiverRequired ?? false) || metadataWaiverRequired === true || stripeCountryRequiresWaiver; const euWithdrawalWaiverAccepted = (localPayment?.euWithdrawalWaiverAccepted ?? false) || metadataWaiverAccepted === true; return { amountPaidCents: amountPaid, chargeId: primaryPayment.charge_id ?? null, currency: invoice.currency ?? 'usd', invoiceId: invoice.provider_id, invoiceCreatedAt: invoice.stripe_created_at ?? new Date(0), paymentCompletedAt: localPayment?.completedAt ?? null, paymentIntentId: primaryPayment.payment_intent_id ?? null, paidAt: primaryPayment.paid_at ?? invoice.paid_at ?? null, purchaseGeoipCountryCode: localPayment?.purchaseGeoipCountryCode ?? normalizeCountryCode(getMetadataString(sessionMetadata, 'purchase_geoip_country_code')), purchaseClientCountryCode: localPayment?.purchaseClientCountryCode ?? normalizeCountryCode(getMetadataString(sessionMetadata, 'purchase_client_country_code')), euWithdrawalWaiverRequired, euWithdrawalWaiverAccepted, euWithdrawalWaiverAcceptedAt: localPayment?.euWithdrawalWaiverAcceptedAt ?? getCheckoutSessionAcceptedAt(checkoutSession), euWithdrawalWaiverTextVersion: localPayment?.euWithdrawalWaiverTextVersion ?? getMetadataString(sessionMetadata, 'eu_withdrawal_waiver_text_version'), stripeBillingCountryCode, stripeCustomerCountryCode, stripePaymentMethodCountryCode, stripeTermsOfServiceAccepted, }; }
async issueRefund(params: { adminUserId: UserID; targetUser: User; paymentIntentId: string; amountCents?: number; reason?: string; }): Promise<void> { if (!this.stripe) { throw new StripePaymentNotAvailableError(); } const customerId = await this.getResolvedStripeCustomerId(params.targetUser); const syncedTargetUser = customerId ? await this.syncResolvedStripeState(params.targetUser, {customerId}) : params.targetUser; await assertOwnedPaymentIntentForAdminRefund( this.userRepository, this.billingRepository, syncedTargetUser, params.paymentIntentId, ); try { const refund = await this.stripe.refunds.create({ payment_intent: params.paymentIntentId, ...(params.amountCents !== undefined ? {amount: params.amountCents} : {}), ...(params.reason ? {reason: 'requested_by_customer' as const} : {}), metadata: { admin_user_id: params.adminUserId.toString(), target_user_id: syncedTargetUser.id.toString(), ...(params.reason ? {admin_reason: params.reason} : {}), }, }); try { await this.billingRepository.refunds.upsertFromStripe(refund, { customerId: syncedTargetUser.stripeCustomerId ?? undefined, userId: syncedTargetUser.id, }); } catch (mirrorErr) { Logger.error({mirrorErr, refundId: refund.id}, 'Mirror upsert failed after admin refund; reconciler will heal'); } } catch (error) { throw new StripeError(error instanceof Error ? error.message : 'Failed to refund payment'); } const metadata = new Map<string, string>([['payment_intent_id', params.paymentIntentId]]); if (params.amountCents !== undefined) { metadata.set('amount_cents', String(params.amountCents)); } if (params.reason) { metadata.set('reason', params.reason); } await this.auditService.createAuditLog({ adminUserId: params.adminUserId, targetType: 'user', targetId: BigInt(syncedTargetUser.id), action: 'billing_refund', auditLogReason: params.reason ?? null, metadata, }); }
async resolveLatestRefundableInvoiceForImmediateCancel( user: User, subscription: BillingSubscriptionRow, ): Promise<AdminImmediateCancelRefundTarget | null> { const localPayments = await this.getUserPayments(user); const localPaymentsByInvoiceId = new Map<string, Payment>(); for (const payment of localPayments) { if (payment.invoiceId && !localPaymentsByInvoiceId.has(payment.invoiceId)) { localPaymentsByInvoiceId.set(payment.invoiceId, payment); } } const expectedCustomerId = user.stripeCustomerId ?? subscription.customer_id ?? null; const customerIds = await this.resolveCustomerIds(user); const tryInvoice = async (invoice: BillingInvoiceRow): Promise<AdminImmediateCancelRefundTarget | null> => { const primaryPayment = await this.billingRepository.payments.findPrimaryForInvoice(invoice.provider_id); const chargeId = primaryPayment?.charge_id ?? null; const paymentIntentId = primaryPayment?.payment_intent_id ?? null; const charge = chargeId ? await this.billingRepository.charges.findById(chargeId) : null; const checkoutSession = await this.getCheckoutSessionForInvoice({ paymentIntentId, subscriptionId: invoice.subscription_id, customerIds, }); return this.buildRefundTargetFromMirror( invoice, primaryPayment, charge, checkoutSession, expectedCustomerId, localPaymentsByInvoiceId.get(invoice.provider_id) ?? null, ); }; if (subscription.latest_invoice_id) { const invoice = await this.billingRepository.invoices.findById(subscription.latest_invoice_id); if (invoice) { const target = await tryInvoice(invoice); if (target) return target; } } const subInvoices = await this.billingRepository.invoices.listBySubscription(subscription.provider_id, { pageSize: 50, }); for (const invoice of subInvoices.rows) { const target = await tryInvoice(invoice); if (target) return target; } for (const lp of localPayments) { if (!lp.invoiceId) continue; const invoice = await this.billingRepository.invoices.findById(lp.invoiceId); if (!invoice) continue; const target = await tryInvoice(invoice); if (target) return target; } return null; }
async applyRefundPolicyAndCancelImmediately(params: { adminUserId: UserID; targetUser: User; reason?: string; }): Promise<AdminBillingRefundLatestInvoiceCancelResponse> { if (!this.stripe || !this.stripeService) { throw new StripePaymentNotAvailableError(); } const subscription = await this.resolveManageableStripeSubscription(params.targetUser); if (!subscription) { throw new StripeNoActiveSubscriptionError(); } const syncedTargetUser = await this.syncResolvedStripeState(params.targetUser, { customerId: subscription.customer_id ?? undefined, subscriptionId: subscription.provider_id, premiumWillCancel: subscription.cancel_at_period_end ?? undefined, }); const refundTarget = await this.resolveLatestRefundableInvoiceForImmediateCancel(syncedTargetUser, subscription); const refundDecision = decideImmediateCancelRefund({ amountPaidCents: refundTarget?.amountPaidCents ?? 0, now: new Date(), refundTarget, subscription, }); const intentId = await this.billingRepository.actionIntents.create({ userId: BigInt(syncedTargetUser.id), actorAdminId: BigInt(params.adminUserId), actionType: 'cancel_and_refund', subscriptionId: subscription.provider_id, invoiceId: refundTarget?.invoiceId ?? null, paymentIntentId: refundTarget?.paymentIntentId ?? null, refundAmount: refundDecision.amountCents !== null ? BigInt(refundDecision.amountCents) : null, refundReason: params.reason ?? null, }); let refund: Stripe.Response<Stripe.Refund> | null = null; try { await this.stripeService.cancelSubscriptionImmediately(syncedTargetUser.id, params.reason); await this.billingRepository.actionIntents.markStage(intentId, 'sub_canceled', { sub_canceled_at: new Date(), }); if (refundDecision.amountCents !== null) { if (!refundTarget) { throw new StripeError('No paid Stripe invoice with a refundable payment was found for this subscription'); } refund = await this.stripe.refunds.create( { ...(refundTarget.paymentIntentId ? {payment_intent: refundTarget.paymentIntentId} : {charge: refundTarget.chargeId!}), amount: refundDecision.amountCents, ...(params.reason ? {reason: 'requested_by_customer' as const} : {}), metadata: { intent_id: String(intentId), admin_user_id: params.adminUserId.toString(), target_user_id: syncedTargetUser.id.toString(), subscription_id: subscription.provider_id, invoice_id: refundTarget.invoiceId, refund_policy: refundDecision.policy, refund_policy_basis: refundDecision.basis, eu_withdrawal_waiver_required: refundTarget.euWithdrawalWaiverRequired ? 'true' : 'false', eu_withdrawal_waiver_accepted: refundTarget.euWithdrawalWaiverAccepted ? 'true' : 'false', ...(refundTarget.purchaseGeoipCountryCode ? {purchase_geoip_country_code: refundTarget.purchaseGeoipCountryCode} : {}), ...(refundTarget.purchaseClientCountryCode ? {purchase_client_country_code: refundTarget.purchaseClientCountryCode} : {}), ...(refundTarget.euWithdrawalWaiverTextVersion ? {eu_withdrawal_waiver_text_version: refundTarget.euWithdrawalWaiverTextVersion} : {}), ...(refundTarget.stripeBillingCountryCode ? {stripe_billing_country_code: refundTarget.stripeBillingCountryCode} : {}), ...(refundTarget.stripePaymentMethodCountryCode ? {stripe_payment_method_country_code: refundTarget.stripePaymentMethodCountryCode} : {}), ...(refundTarget.stripeCustomerCountryCode ? {stripe_customer_country_code: refundTarget.stripeCustomerCountryCode} : {}), ...(refundTarget.stripeTermsOfServiceAccepted !== null ? {stripe_terms_of_service_accepted: refundTarget.stripeTermsOfServiceAccepted ? 'true' : 'false'} : {}), ...(params.reason ? {admin_reason: params.reason} : {}), }, }, {idempotencyKey: `admin-cancel-refund:${intentId}`}, ); try { await this.billingRepository.refunds.upsertFromStripe(refund, { invoiceId: refundTarget.invoiceId, customerId: subscription.customer_id ?? undefined, userId: BigInt(syncedTargetUser.id), }); } catch (mirrorErr) { Logger.error( {mirrorErr, refundId: refund.id}, 'Mirror upsert failed after refund create; reconciler will heal', ); } await this.billingRepository.actionIntents.markStage(intentId, 'refund_created', { refund_created_at: new Date(), refund_id: refund.id, }); } await this.billingRepository.actionIntents.markStage(intentId, 'complete', { completed_at: new Date(), }); } catch (err) { const message = err instanceof Error ? err.message : String(err); try { await this.billingRepository.actionIntents.markStage(intentId, 'failed', { error_message: message, }); } catch (markErr) { Logger.error({markErr, intentId: String(intentId)}, 'Failed to mark action intent as failed'); } throw err; } const auditMetadata = new Map<string, string>([ ['subscription_id', subscription.provider_id], ['refund_policy', refundDecision.policy], ['refund_policy_basis', refundDecision.basis], ['cycle_elapsed_days', String(refundDecision.cycleElapsedDays)], ['intent_id', String(intentId)], ]); if (refundTarget) { auditMetadata.set('invoice_id', refundTarget.invoiceId); auditMetadata.set('invoice_amount_paid_cents', String(refundTarget.amountPaidCents)); if (refundTarget.paymentIntentId) auditMetadata.set('payment_intent_id', refundTarget.paymentIntentId); if (refundTarget.chargeId) auditMetadata.set('charge_id', refundTarget.chargeId); } if (refund) { auditMetadata.set('refund_id', refund.id); auditMetadata.set('refunded_amount_cents', String(refund.amount)); } if (params.reason) { auditMetadata.set('reason', params.reason); } await this.auditService.createAuditLog({ adminUserId: params.adminUserId, targetType: 'user', targetId: BigInt(syncedTargetUser.id), action: 'billing_refund_policy_cancel_now', auditLogReason: params.reason ?? null, metadata: auditMetadata, }); return { subscription_id: subscription.provider_id, invoice_id: refundTarget?.invoiceId ?? '', payment_intent_id: refundTarget?.paymentIntentId ?? null, charge_id: refundTarget?.chargeId ?? null, refund_policy: refundDecision.policy, refund_policy_basis: refundDecision.basis, refund_id: refund?.id ?? null, refunded_amount_cents: refund?.amount ?? 0, invoice_amount_paid_cents: refundTarget?.amountPaidCents ?? 0, currency: refundTarget?.currency ?? subscription.currency ?? 'usd', cycle_elapsed_days: refundDecision.cycleElapsedDays, purchase_geoip_country_code: refundTarget?.purchaseGeoipCountryCode ?? null, purchase_client_country_code: refundTarget?.purchaseClientCountryCode ?? null, stripe_payment_method_country_code: refundTarget?.stripePaymentMethodCountryCode ?? null, stripe_billing_country_code: refundTarget?.stripeBillingCountryCode ?? null, stripe_customer_country_code: refundTarget?.stripeCustomerCountryCode ?? null, stripe_terms_of_service_accepted: refundTarget?.stripeTermsOfServiceAccepted ?? null, eu_withdrawal_waiver_required: refundTarget?.euWithdrawalWaiverRequired ?? false, eu_withdrawal_waiver_accepted: refundTarget?.euWithdrawalWaiverAccepted ?? false, eu_withdrawal_waiver_accepted_at: refundTarget?.euWithdrawalWaiverAcceptedAt?.toISOString() ?? null, eu_withdrawal_waiver_text_version: refundTarget?.euWithdrawalWaiverTextVersion ?? null, }; }
private async ensureManageableSubscription(targetUser: User): Promise<BillingSubscriptionRow> { const sub = await this.resolveManageableStripeSubscription(targetUser); if (!sub) { throw new StripeNoActiveSubscriptionError(); } await this.syncResolvedStripeState(targetUser, { customerId: sub.customer_id ?? undefined, subscriptionId: sub.provider_id, premiumWillCancel: sub.cancel_at_period_end ?? undefined, }); return sub; }
async cancelSubscription(adminUserId: UserID, targetUserId: UserID, auditLogReason: string | null): Promise<void> { if (!this.stripeService) { throw new StripePaymentNotAvailableError(); } const targetUser = await getRequiredUser(this.userRepository, targetUserId); await this.ensureManageableSubscription(targetUser); await this.stripeService.cancelSubscriptionAtPeriodEnd(targetUserId); await this.auditService.createAuditLog({ adminUserId, targetType: 'user', targetId: BigInt(targetUserId), action: 'billing_cancel_subscription', auditLogReason, }); }
async cancelSubscriptionImmediately( adminUserId: UserID, targetUserId: UserID, auditLogReason: string | null, ): Promise<void> { if (!this.stripeService) { throw new StripePaymentNotAvailableError(); } const targetUser = await getRequiredUser(this.userRepository, targetUserId); await this.ensureManageableSubscription(targetUser); await this.stripeService.cancelSubscriptionImmediately(targetUserId, auditLogReason ?? undefined); await this.auditService.createAuditLog({ adminUserId, targetType: 'user', targetId: BigInt(targetUserId), action: 'billing_cancel_subscription_now', auditLogReason, }); }
async endPremiumGracePeriod(adminUserId: UserID, targetUserId: UserID, auditLogReason: string | null): Promise<void> { if (!this.stripeService) { throw new StripePaymentNotAvailableError(); } const targetUser = await getRequiredUser(this.userRepository, targetUserId); const wasInGrace = await this.stripeService.endPremiumGracePeriod(targetUserId); const auditMetadata = new Map<string, string>(); auditMetadata.set('was_in_grace', String(wasInGrace)); if (targetUser.premiumGraceEndsAt) { auditMetadata.set('prior_grace_ends_at', targetUser.premiumGraceEndsAt.toISOString()); } await this.auditService.createAuditLog({ adminUserId, targetType: 'user', targetId: BigInt(targetUserId), action: 'billing_end_premium_grace_period', auditLogReason, metadata: auditMetadata, }); }
async reactivateSubscription( adminUserId: UserID, targetUserId: UserID, auditLogReason: string | null, ): Promise<void> { if (!this.stripeService) { throw new StripePaymentNotAvailableError(); } const targetUser = await getRequiredUser(this.userRepository, targetUserId); await this.ensureManageableSubscription(targetUser); await this.stripeService.reactivateSubscription(targetUserId); await this.auditService.createAuditLog({ adminUserId, targetType: 'user', targetId: BigInt(targetUserId), action: 'billing_reactivate_subscription', auditLogReason, }); }
async getPriceForInvoice(invoice: BillingInvoiceRow): Promise<BillingPriceRow | null> { if (!invoice.subscription_id) return null; const sub = await this.billingRepository.subscriptions.findById(invoice.subscription_id); if (!sub?.primary_price_id) return null; return this.billingRepository.prices.findById(sub.primary_price_id); }}
function createBillingService(ctx: {get: <K extends keyof HonoEnv['Variables']>(key: K) => HonoEnv['Variables'][K]}): { userRepository: IUserRepository; service: BillingAdminControllerService;} { const userRepository = ctx.get('userRepository'); const stripeService = (ctx.get('stripeService') as StripeService | undefined) ?? null; const snowflakeService = ctx.get('snowflakeService') as ISnowflakeService; const auditService = new AdminAuditService(new AdminRepository(), snowflakeService); const billingRepository = getBillingRepository(); return { userRepository, service: new BillingAdminControllerService(userRepository, stripeService, auditService, billingRepository), };}
function mapMirrorPaymentRecordToResponse(user: User, record: MirrorBillingPaymentRecord) { const checkoutSessionMetadata = getCheckoutSessionMetadata(record.checkoutSession); const localPayment = record.localPayment; const resolvedPaymentIntentId = record.primaryPayment?.payment_intent_id ?? null; const refundedAmountCents = getRefundAmount(record.refunds); const stripePaymentMethodCountryCode = normalizeCountryCode(record.charge?.card_country ?? null); const stripeBillingCountryCode: string | null = null; const stripeCustomerCountryCode: string | null = null; const stripeTermsOfServiceAccepted: boolean | null = null; const metadataWaiverRequired = parseStripeMetadataBoolean( getMetadataString(checkoutSessionMetadata, 'eu_withdrawal_waiver_required'), ); const metadataWaiverAccepted = parseStripeMetadataBoolean( getMetadataString(checkoutSessionMetadata, 'eu_withdrawal_waiver_accepted'), ); const stripeCountryRequiresWaiver = [ stripeBillingCountryCode, stripeCustomerCountryCode, stripePaymentMethodCountryCode, ].some((countryCode) => isEuEeaCountryCode(countryCode)); const euWithdrawalWaiverRequired = (localPayment?.euWithdrawalWaiverRequired ?? false) || metadataWaiverRequired === true || stripeCountryRequiresWaiver; const euWithdrawalWaiverAccepted = (localPayment?.euWithdrawalWaiverAccepted ?? false) || metadataWaiverAccepted === true; const amountPaid = Number(record.invoice.amount_paid ?? 0n); return { checkout_session_id: record.checkoutSession?.provider_id ?? null, user_id: user.id.toString(), stripe_customer_id: record.invoice.customer_id ?? null, payment_intent_id: resolvedPaymentIntentId, resolved_payment_intent_id: resolvedPaymentIntentId, charge_id: record.primaryPayment?.charge_id ?? null, subscription_id: record.invoice.subscription_id ?? null, invoice_id: record.invoice.provider_id, price_id: null, product_type: getMetadataString(checkoutSessionMetadata, 'product_type') ?? record.invoice.billing_reason ?? null, amount_cents: amountPaid, currency: record.invoice.currency ?? 'usd', status: buildPaymentStatus(record.invoice, record.primaryPayment, record.refunds), stripe_source: 'invoice' as const, refundable_via_payment_intent: resolvedPaymentIntentId !== null && refundedAmountCents < amountPaid, refunded_amount_cents: refundedAmountCents, net_amount_cents: Math.max(0, amountPaid - refundedAmountCents), refunds: record.refunds.map((refund) => ({ id: refund.provider_id, amount_cents: Number(refund.amount ?? 0n), currency: refund.currency ?? 'usd', status: refund.status ?? null, reason: refund.reason ?? null, created: refund.stripe_created_at ? Math.floor(refund.stripe_created_at.getTime() / 1000) : 0, payment_intent_id: refund.payment_intent_id ?? null, charge_id: refund.charge_id ?? null, })), payment_method_type: record.charge?.payment_method_type ?? null, payment_method_brand: record.charge?.card_brand ?? null, payment_method_last4: record.charge?.card_last4 ?? null, stripe_payment_method_country_code: stripePaymentMethodCountryCode, stripe_billing_country_code: stripeBillingCountryCode, stripe_customer_country_code: stripeCustomerCountryCode, stripe_terms_of_service_accepted: stripeTermsOfServiceAccepted, is_gift: getMetadataString(checkoutSessionMetadata, 'is_gift') === 'true', gift_code: getMetadataString(checkoutSessionMetadata, 'gift_code'), purchase_geoip_country_code: normalizeCountryCode( getMetadataString(checkoutSessionMetadata, 'purchase_geoip_country_code'), ), purchase_client_country_code: normalizeCountryCode( getMetadataString(checkoutSessionMetadata, 'purchase_client_country_code'), ), eu_withdrawal_waiver_required: euWithdrawalWaiverRequired, eu_withdrawal_waiver_accepted: euWithdrawalWaiverAccepted, eu_withdrawal_waiver_accepted_at: getCheckoutSessionAcceptedAt(record.checkoutSession)?.toISOString() ?? null, eu_withdrawal_waiver_text_version: getMetadataString(checkoutSessionMetadata, 'eu_withdrawal_waiver_text_version'), created_at: (record.invoice.stripe_created_at ?? new Date(0)).toISOString(), completed_at: record.primaryPayment?.paid_at?.toISOString() ?? (record.invoice.status === 'paid' ? ((record.invoice.paid_at ?? record.invoice.stripe_created_at ?? null)?.toISOString() ?? null) : null), };}
function mapInvoiceRowToResponse(invoice: BillingInvoiceRow, primaryPayment: BillingPaymentRow | null) { return { id: invoice.provider_id, amount_due: Number(invoice.amount_due ?? 0n), amount_paid: Number(invoice.amount_paid ?? 0n), currency: invoice.currency ?? 'usd', status: invoice.status ?? null, created: invoice.stripe_created_at ? Math.floor(invoice.stripe_created_at.getTime() / 1000) : 0, billing_reason: invoice.billing_reason ?? null, subscription_id: invoice.subscription_id ?? null, payment_type: null, payment_status: primaryPayment?.status ?? null, payment_intent_id: primaryPayment?.payment_intent_id ?? null, charge_id: primaryPayment?.charge_id ?? null, paid_at: (primaryPayment?.paid_at ?? invoice.paid_at)?.toISOString() ?? null, hosted_invoice_url: invoice.hosted_invoice_url ?? null, invoice_pdf: invoice.invoice_pdf ?? null, };}
function mapSubscriptionRowToResponse(sub: BillingSubscriptionRow, primaryPrice: BillingPriceRow | null) { return { id: sub.provider_id, status: sub.status ?? 'unknown', current_period_start: sub.current_period_start?.toISOString() ?? null, current_period_end: sub.current_period_end?.toISOString() ?? null, cancel_at_period_end: sub.cancel_at_period_end ?? false, cancel_at: sub.cancel_at?.toISOString() ?? null, canceled_at: sub.canceled_at?.toISOString() ?? null, plan_interval: primaryPrice?.interval ?? null, plan_amount_cents: primaryPrice?.unit_amount !== null && primaryPrice?.unit_amount !== undefined ? Number(primaryPrice.unit_amount) : null, plan_currency: primaryPrice?.currency ?? sub.currency ?? null, default_payment_method_id: sub.default_payment_method ?? null, };}
function mapPaymentMethodRowToResponse(pm: BillingPaymentMethodRow) { return { id: pm.provider_id, type: pm.type ?? 'card', card_brand: pm.card_brand ?? null, card_last4: pm.card_last4 ?? null, card_exp_month: pm.card_exp_month ?? null, card_exp_year: pm.card_exp_year ?? null, created: pm.stripe_created_at ? Math.floor(pm.stripe_created_at.getTime() / 1000) : 0, };}
function buildEmptySubscriptionResponse() { return { id: '', status: 'none', current_period_start: null, current_period_end: null, cancel_at_period_end: false, cancel_at: null, canceled_at: null, plan_interval: null, plan_amount_cents: null, plan_currency: null, default_payment_method_id: null, };}
function clampInvoiceLimit(rawLimit: string | undefined): number { if (!rawLimit) return 25; const parsed = parseInt(rawLimit, 10); if (!Number.isFinite(parsed)) return 25; return Math.max(1, Math.min(parsed, 100));}
export function BillingAdminController(app: HonoApp) { app.get( '/admin/billing/users/:userId/overview', requireAdminACL(AdminACLs.BILLING_VIEW), OpenAPI({ operationId: 'admin_billing_overview', summary: 'Get billing overview for a user', description: 'Retrieve subscription status, payment history, and Stripe payment methods for a user.', responseSchema: AdminBillingOverviewResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const [payments, subscription, paymentMethods, stripeCustomerId] = await Promise.all([ billingService.getMirrorBillingPaymentRecords(user), billingService.getUserSubscription(user), billingService.getUserPaymentMethods(user), billingService.getResolvedStripeCustomerId(user), ]); const subscriptionPrice = subscription ? await getBillingRepository().prices.findById(subscription.primary_price_id ?? '') : null; return ctx.json({ subscription: subscription ? mapSubscriptionRowToResponse(subscription, subscriptionPrice) : null, payments: payments.map((payment) => mapMirrorPaymentRecordToResponse(user, payment)), payment_methods: paymentMethods.map(mapPaymentMethodRowToResponse), stripe_customer_id: stripeCustomerId, }); }, ); app.get( '/admin/billing/guilds/:guildId/overview', requireAdminACL(AdminACLs.BILLING_VIEW), OpenAPI({ operationId: 'admin_billing_guild_overview', summary: 'Get billing overview for a guild', description: 'Retrieve guild billing state. The current billing mirror is user-scoped, so guilds without persisted billing records return an empty overview.', responseSchema: AdminBillingOverviewResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const guildId = ctx.req.param('guildId'); const adminService = ctx.get('adminService'); const lookup = await adminService.guildServiceAggregate.lookupService.lookupGuild({guild_id: BigInt(guildId)}); if (!lookup.guild) { throw new UnknownGuildError(); } return ctx.json({ subscription: null, payments: [], payment_methods: [], stripe_customer_id: null, }); }, ); app.get( '/admin/billing/users/:userId/payments', requireAdminACL(AdminACLs.BILLING_VIEW), OpenAPI({ operationId: 'admin_billing_list_payments', summary: 'List payments for a user', description: 'Retrieve the payment history stored for a user.', responseSchema: AdminPaymentListResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const payments = await billingService.getMirrorBillingPaymentRecords(user); return ctx.json({ payments: payments.map((payment) => mapMirrorPaymentRecordToResponse(user, payment)), }); }, ); app.get( '/admin/billing/users/:userId/subscription', requireAdminACL(AdminACLs.BILLING_VIEW), OpenAPI({ operationId: 'admin_billing_get_subscription', summary: 'Get subscription for a user', description: 'Retrieve the current Stripe subscription details for a user.', responseSchema: AdminSubscriptionResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const subscription = await billingService.getUserSubscription(user); const subscriptionPrice = subscription?.primary_price_id ? await getBillingRepository().prices.findById(subscription.primary_price_id) : null; return ctx.json( subscription ? mapSubscriptionRowToResponse(subscription, subscriptionPrice) : buildEmptySubscriptionResponse(), ); }, ); app.get( '/admin/billing/users/:userId/payment-methods', requireAdminACL(AdminACLs.BILLING_VIEW), OpenAPI({ operationId: 'admin_billing_list_payment_methods', summary: 'List payment methods for a user', description: 'Retrieve the Stripe payment methods associated with a user.', responseSchema: AdminPaymentMethodListResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const paymentMethods = await billingService.getUserPaymentMethods(user); return ctx.json({ payment_methods: paymentMethods.map(mapPaymentMethodRowToResponse), }); }, ); app.get( '/admin/billing/users/:userId/invoices', requireAdminACL(AdminACLs.BILLING_VIEW), OpenAPI({ operationId: 'admin_billing_list_invoices', summary: 'List invoices for a user', description: 'Retrieve recent Stripe invoices for a user.', responseSchema: AdminInvoiceListResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const limit = clampInvoiceLimit(ctx.req.query('limit') ?? undefined); const result = await billingService.getUserInvoices(user, limit); const billingRepo = getBillingRepository(); const invoiceResponses = await Promise.all( result.invoices.map(async (invoice) => { const primaryPayment = await billingRepo.payments.findPrimaryForInvoice(invoice.provider_id); return mapInvoiceRowToResponse(invoice, primaryPayment); }), ); return ctx.json({ invoices: invoiceResponses, has_more: result.has_more, }); }, ); app.post( '/admin/billing/users/:userId/refund', requireAdminACL(AdminACLs.BILLING_REFUND), Validator('json', AdminBillingRefundRequest), OpenAPI({ operationId: 'admin_billing_refund', summary: 'Issue a refund for a user payment', description: 'Issue a full or partial refund for a user payment through Stripe.', responseSchema: null, statusCode: 204, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const {payment_intent_id, amount_cents, reason} = ctx.req.valid('json'); await billingService.issueRefund({ adminUserId: ctx.get('adminUserId'), targetUser: user, paymentIntentId: payment_intent_id, amountCents: amount_cents, reason: reason ?? undefined, }); return ctx.body(null, 204); }, ); app.post( '/admin/billing/users/:userId/refund-policy-cancel-now', requireAdminACL(AdminACLs.BILLING_REFUND), requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), Validator('json', AdminBillingRefundLatestInvoiceCancelRequest), OpenAPI({ operationId: 'admin_billing_refund_policy_cancel_now', summary: 'Apply refund policy and cancel subscription immediately', description: 'Cancels a user subscription immediately and applies the support refund policy against the latest paid Stripe invoice.', responseSchema: AdminBillingRefundLatestInvoiceCancelResponse, statusCode: 200, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {userRepository, service: billingService} = createBillingService(ctx); const user = await getRequiredUser(userRepository, userId); const {reason} = ctx.req.valid('json'); const result = await billingService.applyRefundPolicyAndCancelImmediately({ adminUserId: ctx.get('adminUserId'), targetUser: user, reason: reason ?? undefined, }); return ctx.json(result); }, ); app.post( '/admin/billing/users/:userId/cancel-subscription-now', requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), Validator('json', AdminBillingCancelImmediatelyRequest), OpenAPI({ operationId: 'admin_billing_cancel_subscription_now', summary: 'Cancel a user subscription immediately', description: 'Cancel a user Stripe subscription immediately without issuing a refund.', responseSchema: null, statusCode: 204, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {service: billingService} = createBillingService(ctx); const {reason} = ctx.req.valid('json'); await billingService.cancelSubscriptionImmediately(ctx.get('adminUserId'), userId, reason ?? null); return ctx.body(null, 204); }, ); app.post( '/admin/billing/users/:userId/cancel-subscription', requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), OpenAPI({ operationId: 'admin_billing_cancel_subscription', summary: 'Cancel a user subscription', description: 'Set a user Stripe subscription to cancel at period end.', responseSchema: null, statusCode: 204, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {service: billingService} = createBillingService(ctx); await billingService.cancelSubscription(ctx.get('adminUserId'), userId, ctx.get('auditLogReason')); return ctx.body(null, 204); }, ); app.post( '/admin/billing/users/:userId/end-premium-grace-period', requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), OpenAPI({ operationId: 'admin_billing_end_premium_grace_period', summary: "End a user's premium grace period", description: 'End the post-cancel premium grace period for a user immediately, downgrading them and clearing premium_since. Idempotent: safe to call when not in grace. Use when investigating fraud or honoring a user request to opt out of the recovery window.', responseSchema: null, statusCode: 204, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {service: billingService} = createBillingService(ctx); await billingService.endPremiumGracePeriod(ctx.get('adminUserId'), userId, ctx.get('auditLogReason')); return ctx.body(null, 204); }, ); app.post( '/admin/billing/users/:userId/reactivate-subscription', requireAdminACL(AdminACLs.BILLING_MANAGE_SUBSCRIPTION), OpenAPI({ operationId: 'admin_billing_reactivate_subscription', summary: 'Reactivate a user subscription', description: 'Remove a period-end cancellation from a user Stripe subscription.', responseSchema: null, statusCode: 204, security: 'adminApiKey', tags: 'Admin', }), async (ctx) => { ensureBillingFeatureAvailable(); const userId = createUserID(BigInt(ctx.req.param('userId'))); const {service: billingService} = createBillingService(ctx); await billingService.reactivateSubscription(ctx.get('adminUserId'), userId, ctx.get('auditLogReason')); return ctx.body(null, 204); }, );}