Phase7: Void/Refund logic
Some checks failed
Build and Push / build (push) Failing after 52s

This commit is contained in:
2026-05-25 21:54:51 +02:00
parent 9cd9879a18
commit e15a668f8e
19 changed files with 2679 additions and 41 deletions

View File

@@ -258,3 +258,57 @@ export async function createCheckoutSessionForInvoice(params: {
}
return { url: session.url, sessionId: session.id };
}
// ---------------------------------------------------------------------------
// Phase 7 — refunds
// ---------------------------------------------------------------------------
/**
* Create a Stripe Refund against an invoice's PaymentIntent.
*
* The amount is in CHF; we convert to rappen for Stripe's smallest-
* currency-unit API. Pass 0 or undefined for `amountChf` to refund
* the full charge.
*
* Returns the Stripe refund object so the caller can record the
* refund id and final status. Stripe processes refunds asynchronously
* for some payment methods, so the initial status may be 'pending'
* — the charge.refunded webhook delivers the eventual succeeded /
* failed transition.
*
* Throws on Stripe API errors (no charge, insufficient balance,
* etc.). The caller surfaces these to the admin via the API
* response — we don't swallow them because partial-refund logic
* shouldn't be guessing about server state.
*/
export async function createInvoiceRefund(params: {
paymentIntentId: string;
amountChf?: number;
reason?: "duplicate" | "fraudulent" | "requested_by_customer";
metadata?: Record<string, string>;
}): Promise<{
id: string;
amountChf: number;
status: string;
}> {
const stripe = getStripeClient();
const refundParams: Parameters<typeof stripe.refunds.create>[0] = {
payment_intent: params.paymentIntentId,
metadata: params.metadata,
};
if (params.amountChf && params.amountChf > 0) {
refundParams.amount = chfToRappen(params.amountChf);
}
if (params.reason) {
refundParams.reason = params.reason;
}
const refund = await stripe.refunds.create(refundParams);
// The amount on the response is in rappen; convert back. If no
// amount was passed, Stripe defaults to the full remaining
// charge, which is what we read back.
return {
id: refund.id,
amountChf: refund.amount != null ? refund.amount / 100 : 0,
status: refund.status ?? "unknown",
};
}