From c7867ab05eaec1659cb76d29ee2e4b4426cbcee7 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 7 Apr 2026 18:05:30 +0700 Subject: [PATCH] feat: download invoices --- .../src/components/layout/impersonate.tsx | 31 +++++++++++++++++++ .../src/services/stripe.service.ts | 28 ++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/layout/impersonate.tsx b/apps/frontend/src/components/layout/impersonate.tsx index d50f3e06..7f9bf46d 100644 --- a/apps/frontend/src/components/layout/impersonate.tsx +++ b/apps/frontend/src/components/layout/impersonate.tsx @@ -22,6 +22,8 @@ interface Charge { refunded: boolean; amount_refunded: number; description: string | null; + receipt_url: string | null; + invoice_pdf: string | null; } const useCharges = () => { @@ -124,6 +126,7 @@ const ChargesModal: FC<{ close: () => void }> = ({ close }) => { {t('date', 'Date')} {t('amount', 'Amount')} {t('status', 'Status')} + @@ -178,6 +181,34 @@ const ChargesModal: FC<{ close: () => void }> = ({ close }) => { )} + + {(charge.invoice_pdf || charge.receipt_url) && ( + e.stopPropagation()} + className="inline-flex items-center justify-center w-[28px] h-[28px] rounded-[4px] hover:bg-tableBorder transition-colors" + title={charge.invoice_pdf ? t('download_invoice', 'Download Invoice') : t('view_receipt', 'View Receipt')} + > + + + + + + + )} + ))} diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index 05fa74df..f21bd66c 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -856,7 +856,7 @@ export class StripeService { limit: 100, }); - return charges.data + const chargeList = charges.data .filter((f) => f.status === 'succeeded') .map((charge) => ({ id: charge.id, @@ -867,7 +867,33 @@ export class StripeService { refunded: charge.refunded, amount_refunded: charge.amount_refunded, description: charge.description, + receipt_url: charge.receipt_url || null, + invoice: (charge as any).invoice || null, })); + + const invoiceIds = chargeList + .map((c) => c.invoice) + .filter((id): id is string => !!id && typeof id === 'string'); + + const invoicePdfMap: Record = {}; + for (const invoiceId of invoiceIds) { + try { + const inv = await stripe.invoices.retrieve(invoiceId); + if (inv.invoice_pdf) { + invoicePdfMap[invoiceId] = inv.invoice_pdf; + } + } catch { + // ignore if invoice can't be fetched + } + } + + return chargeList.map((charge) => ({ + ...charge, + invoice_pdf: + charge.invoice && invoicePdfMap[charge.invoice as string] + ? invoicePdfMap[charge.invoice as string] + : null, + })); } async refundCharges(organizationId: string, chargeIds: string[]) {