From 6fed5b083b6cb6ac16ba8a510947acca201a2cdc Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 25 May 2026 22:39:27 +0200 Subject: [PATCH] Phase7: Void/Refund logic --- .../admin/billing/invoice-detail-view.tsx | 93 +++++---- src/lib/billing-pdf.tsx | 92 +-------- src/lib/credit-note-pdf.tsx | 183 +++++------------- src/lib/db.ts | 30 +++ src/lib/pdf-brand.tsx | 126 ++++++++++++ src/messages/de.json | 9 +- src/messages/en.json | 9 +- src/messages/fr.json | 9 +- src/messages/it.json | 9 +- 9 files changed, 292 insertions(+), 268 deletions(-) create mode 100644 src/lib/pdf-brand.tsx diff --git a/src/components/admin/billing/invoice-detail-view.tsx b/src/components/admin/billing/invoice-detail-view.tsx index 57a6390..88eebe2 100644 --- a/src/components/admin/billing/invoice-detail-view.tsx +++ b/src/components/admin/billing/invoice-detail-view.tsx @@ -356,45 +356,60 @@ export function InvoiceDetailView({ detail, creditNotes = [] }: Props) { max: remainingRefundable.toFixed(2), })} -
- setRefundAmount(e.target.value)} - className="w-28 px-3 py-1.5 rounded-md border border-border bg-surface-2 text-sm font-mono" - autoFocus - /> - setRefundReason(e.target.value)} - maxLength={500} - className="flex-grow px-3 py-1.5 rounded-md border border-border bg-surface-2 text-sm" - /> - - +
+
+ + setRefundAmount(e.target.value)} + className="w-32 px-3 py-1.5 rounded-md border border-border bg-surface-2 text-sm font-mono" + autoFocus + /> + + {t("refundAmountInclVatHint")} + +
+
+ + setRefundReason(e.target.value)} + maxLength={500} + className="w-full px-3 py-1.5 rounded-md border border-border bg-surface-2 text-sm" + /> +
+
+ + +
)} diff --git a/src/lib/billing-pdf.tsx b/src/lib/billing-pdf.tsx index 3484a2c..d9e6096 100644 --- a/src/lib/billing-pdf.tsx +++ b/src/lib/billing-pdf.tsx @@ -31,44 +31,18 @@ import { Text, View, StyleSheet, - Svg, - Polygon, - Polyline, renderToBuffer, } from "@react-pdf/renderer"; import type { Invoice, InvoiceLine, InvoiceLineKind } from "@/types"; +import { BRAND, Logo } from "./pdf-brand"; // --------------------------------------------------------------------------- -// Brand constants — edit here to tweak look without touching layout +// Brand: imported from lib/pdf-brand. Edit there to change issuer +// info, colours, or the logo. Both billing-pdf.tsx and credit-note-pdf.tsx +// share the same source of truth so a brand change applies to every +// PDF the portal produces. // --------------------------------------------------------------------------- -const BRAND = { - name: "PieCed IT", - // Primary emerald — matches the logo SVG fill (#10B981). - primary: "#10B981", - // Slightly darker emerald for headings. - primaryDark: "#0a8060", - textColor: "#1a1a1a", - mutedColor: "#666", - borderColor: "#d4d4d4", - // Issuer block — change these to your real legal info. - issuer: { - legalName: "PieCed IT", - addressLine1: "Cedric Mosimann", - addressLine2: "[Strasse Nr.]", - postalCity: "[PLZ] Basel", - country: "Switzerland", - email: "billing@pieced.ch", - web: "pieced.ch", - // Show "MWST-Nr. ..." on PDF when set. - vatNumber: null as string | null, - // Bank instructions — Phase 7 replaces with QR-bill. - bankName: "[Bank name]", - bankIban: "[CHxx xxxx xxxx xxxx xxxx x]", - bankBic: "[BIC]", - }, -}; - // --------------------------------------------------------------------------- // Localized strings // --------------------------------------------------------------------------- @@ -358,62 +332,6 @@ const styles = StyleSheet.create({ }); // --------------------------------------------------------------------------- -// Logo — inlined SVG primitives -// --------------------------------------------------------------------------- - -/** - * PieCed honeycomb logo. Re-renders the same 6-hex glyph as the - * portal's `public/pieced-logo.svg` using React-PDF's SVG support. - * Width/height are independent of the original viewBox so we can - * scale it without losing stroke quality. - */ -const Logo = ({ size = 60 }: { size?: number }) => ( - - {/* H1 solid */} - - {/* H2 outline */} - - {/* H3 outline */} - - {/* H4 solid */} - - {/* H5 partial */} - - {/* H6 partial */} - - -); - // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- diff --git a/src/lib/credit-note-pdf.tsx b/src/lib/credit-note-pdf.tsx index d94e4a4..dc88cd4 100644 --- a/src/lib/credit-note-pdf.tsx +++ b/src/lib/credit-note-pdf.tsx @@ -1,22 +1,17 @@ /** * Credit-note PDF rendering via @react-pdf/renderer. * - * Phase 7. Mirrors billing-pdf.tsx in layout but with: - * - Title "Gutschrift" / "Credit note" / "Note de crédit" / "Nota di credito" - * - Red accent (vs invoice's emerald) so the document is visually - * unmistakable from an invoice - * - References the original invoice number prominently - * - One amount line ("Refund for invoice 2026-00042" or - * "Voided invoice 2026-00042") with VAT broken out - * - Optional reason text below the amount - * - No bank-transfer instructions (refunds flow the other way: - * either Stripe → customer's card, or PieCed → customer's bank - * for invoice-paid cases — neither requires the customer to - * do anything) + * Phase 7. Renders the same brand identity as the invoice PDF + * (hexagon logo, issuer block, layout) with one accent override: + * red instead of emerald. That difference is enough to make voids + * and refunds visually unmistakable from an invoice at a glance, + * while keeping every other element (logo shape, fonts, structure, + * issuer info, page footer) identical so the document family reads + * as one brand. * - * Issuer block and brand constants are intentionally duplicated - * from billing-pdf.tsx for now. A future refactor can hoist them - * into lib/pdf-brand.ts; doing so today is out of scope. + * Brand + Logo come from lib/pdf-brand. Edit there to change + * issuer info, colours, or the logo glyph — both invoice and + * credit-note PDFs pick the changes up. */ import React from "react"; @@ -26,71 +21,32 @@ import { Text, View, StyleSheet, - Svg, - Polygon, - Polyline, renderToBuffer, } from "@react-pdf/renderer"; import type { CreditNote, Invoice } from "@/types"; +import { BRAND, Logo, ACCENT_CREDIT_NOTE } from "./pdf-brand"; + +const ACCENT = ACCENT_CREDIT_NOTE; // --------------------------------------------------------------------------- -// Brand constants — keep in sync with billing-pdf.tsx until the -// shared brand module lands. -// --------------------------------------------------------------------------- - -const BRAND = { - name: "PieCed IT", - // Red accent for credit notes — visually distinct from the invoice - // emerald so customers can't confuse the two at a glance. - primary: "#DC2626", - primaryDark: "#991B1B", - textColor: "#1a1a1a", - mutedColor: "#666", - borderColor: "#d4d4d4", - issuer: { - legalName: "PieCed IT", - addressLine1: "Cedric Mosimann", - addressLine2: "[Strasse Nr.]", - postalCity: "[PLZ] Basel", - country: "Switzerland", - email: "billing@pieced.ch", - web: "pieced.ch", - vatNumber: null as string | null, - }, -}; - -// --------------------------------------------------------------------------- -// Localized strings (mirrors PdfStrings in billing-pdf.tsx, adapted -// for the credit-note context) +// Localized strings // --------------------------------------------------------------------------- interface CreditNoteStrings { - /** Document title at the top — "Gutschrift" etc. */ creditNote: string; - /** "Credit note no." label */ creditNoteNumber: string; - /** "Issue date" label */ issueDate: string; - /** "Bill to" label, same as invoice */ billTo: string; - /** "Attn:" / "z.Hd." prefix */ attentionPrefix: string; - /** "Reference invoice" — links the credit note back to the original */ referenceInvoice: string; - /** "Reason" label for the free-text reason block */ reason: string; - /** Body text describing what this credit note is for. Takes the - * invoice number as a `{number}` placeholder. */ voidLineLabel: string; refundLineLabel: string; - /** Totals labels — same words as invoice but separated for clarity */ subtotal: string; vatLabel: string; totalCredited: string; - /** Footer note explaining the document */ footerVoidNote: string; footerRefundNote: string; - /** VAT note (reverse-charge or normal) — same logic as invoice */ vatNoteSwiss: string; vatNoteReverseCharge: string; vatNoteOutOfScope: string; @@ -195,51 +151,38 @@ const MESSAGES: Record = { }, }; -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - function pickStrings(locale: string): CreditNoteStrings { return MESSAGES[locale] ?? MESSAGES.de; } +// Swiss number formatting — matches billing-pdf for consistency function fmtChf(n: number): string { - return n.toLocaleString("de-CH", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); + const fixed = n.toFixed(2); + const [intPart, decPart] = fixed.split("."); + const withSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, "'"); + return decPart ? `${withSep}.${decPart}` : withSep; } function fmtDate(iso: string, locale: string): string { - const d = new Date(iso); - const localeMap: Record = { - de: "de-CH", - en: "en-GB", - fr: "fr-CH", - it: "it-CH", - }; - return d.toLocaleDateString(localeMap[locale] ?? "de-CH", { - year: "numeric", - month: "long", - day: "numeric", - }); + const [y, m, d] = iso.split("T")[0].split("-").map(Number); + if (locale === "en") { + return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + return `${String(d).padStart(2, "0")}.${String(m).padStart(2, "0")}.${y}`; } function pickVatNote( invoice: Invoice, strings: CreditNoteStrings ): string | null { - // Mirror the invoice's VAT note logic — the credit note's VAT - // treatment must match the original invoice's, otherwise the - // accounting wouldn't reconcile. const country = invoice.billingSnapshot.country?.toUpperCase(); const hasVat = invoice.billingSnapshot.vatNumber?.trim(); - if (country === "CH" || country === "LI") { - return strings.vatNoteSwiss; - } - if (hasVat) { - return strings.vatNoteReverseCharge; - } + if (country === "CH" || country === "LI") return strings.vatNoteSwiss; + if (hasVat) return strings.vatNoteReverseCharge; return strings.vatNoteOutOfScope; } @@ -264,15 +207,15 @@ const styles = StyleSheet.create({ logoBlock: { flexDirection: "row", alignItems: "center" }, brandName: { fontSize: 16, - color: BRAND.primaryDark, + color: ACCENT.primaryDark, marginLeft: 8, fontFamily: "Helvetica-Bold", }, issuerBlock: { textAlign: "right", fontSize: 8.5, color: BRAND.mutedColor }, - issuerName: { fontSize: 11, color: BRAND.primaryDark, marginBottom: 2 }, + issuerName: { fontSize: 11, color: ACCENT.primaryDark, marginBottom: 2 }, docTitle: { fontSize: 22, - color: BRAND.primaryDark, + color: ACCENT.primaryDark, marginBottom: 8, fontFamily: "Helvetica-Bold", }, @@ -289,7 +232,7 @@ const styles = StyleSheet.create({ padding: 8, backgroundColor: "#fdf2f2", borderLeftWidth: 3, - borderLeftColor: BRAND.primary, + borderLeftColor: ACCENT.primary, }, billToLabel: { fontSize: 8, color: BRAND.mutedColor, marginBottom: 4 }, billToName: { fontSize: 11, marginBottom: 2 }, @@ -302,7 +245,7 @@ const styles = StyleSheet.create({ }, amountHeader: { flexDirection: "row", - backgroundColor: BRAND.primaryDark, + backgroundColor: ACCENT.primaryDark, color: "#ffffff", paddingVertical: 5, paddingHorizontal: 6, @@ -318,11 +261,7 @@ const styles = StyleSheet.create({ }, amountDesc: { flex: 1 }, amountValue: { width: 90, textAlign: "right" }, - totals: { - marginLeft: "auto", - width: 220, - marginBottom: 20, - }, + totals: { marginLeft: "auto", width: 220, marginBottom: 20 }, totalsRow: { flexDirection: "row", justifyContent: "space-between", @@ -334,17 +273,17 @@ const styles = StyleSheet.create({ flexDirection: "row", justifyContent: "space-between", borderTopWidth: 1, - borderTopColor: BRAND.primaryDark, + borderTopColor: ACCENT.primaryDark, paddingTop: 6, marginTop: 4, }, totalsGrandLabel: { - color: BRAND.primaryDark, + color: ACCENT.primaryDark, fontSize: 11, fontFamily: "Helvetica-Bold", }, totalsGrandValue: { - color: BRAND.primaryDark, + color: ACCENT.primaryDark, fontSize: 11, textAlign: "right", fontFamily: "Helvetica-Bold", @@ -386,26 +325,6 @@ const styles = StyleSheet.create({ }, }); -// Mini SVG logo — identical shape to billing-pdf, just recolored -// to the credit-note red so it matches the document accent. -function Logo() { - return ( - - - - - ); -} - -// --------------------------------------------------------------------------- -// Main document -// --------------------------------------------------------------------------- - interface CreditNotePdfProps { creditNote: CreditNote; invoice: Invoice; @@ -417,20 +336,24 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { const vatNote = pickVatNote(invoice, strings); const amountLabelTemplate = creditNote.kind === "void" ? strings.voidLineLabel : strings.refundLineLabel; - const amountLabel = amountLabelTemplate.replace("{number}", invoice.invoiceNumber); + const amountLabel = amountLabelTemplate.replace( + "{number}", + invoice.invoiceNumber + ); const footerNote = creditNote.kind === "void" ? strings.footerVoidNote : strings.footerRefundNote; - // Subtotal + VAT breakdown. We carry the VAT proportion from the - // credit note row itself (set by billing.ts based on the invoice's - // VAT rate × the refund/void amount). + // Stored convention: amount_chf is gross (incl. VAT), + // vat_amount_chf is the VAT portion. Subtotal computed for + // display. const subtotal = creditNote.amountChf - creditNote.vatAmountChf; return ( - {/* Header: logo+brand left, issuer block right */} + {/* Header — SAME hexagon logo as the invoice, tinted red. + Issuer block from BRAND.issuer (shared with invoice). */} - + {BRAND.name} @@ -449,7 +372,6 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { {strings.creditNote} - {/* Meta row: number, issue date, reference invoice */} {strings.creditNoteNumber} @@ -467,7 +389,6 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { - {/* Bill-to (mirrors invoice block) */} {strings.billTo} {snap.companyName} @@ -484,7 +405,6 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { {snap.vatNumber && MWST/VAT: {snap.vatNumber}} - {/* Amount line — single row, the credit note isn't itemized */} @@ -496,7 +416,6 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { - {/* Totals */} {strings.subtotal} @@ -520,7 +439,6 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { - {/* Reason block — only if the admin provided one */} {creditNote.reason && creditNote.reason.trim().length > 0 && ( {strings.reason} @@ -528,13 +446,11 @@ function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) { )} - {/* Footer note explaining what this document is */} {footerNote} {vatNote && {vatNote}} - {/* Tiny footer with credit-note number on every page */} {BRAND.issuer.legalName} · {creditNote.creditNoteNumber} @@ -548,6 +464,5 @@ export async function renderCreditNotePdf( invoice: Invoice ): Promise { const doc = ; - // @react-pdf/renderer's renderToBuffer returns a Node Buffer. return renderToBuffer(doc) as unknown as Buffer; } diff --git a/src/lib/db.ts b/src/lib/db.ts index 719d129..4e1f76e 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -578,6 +578,36 @@ const MIGRATION_SQL = ` CREATE INDEX IF NOT EXISTS idx_invoice_refunds_invoice ON invoice_refunds(invoice_id); + -- Phase 7 fix: the credit_notes.invoice_id and + -- invoice_refunds.invoice_id FKs were originally created without + -- ON DELETE CASCADE, which made admin "delete invoice" fail with + -- a FK violation when the invoice had any voids/refunds attached. + -- The production policy is to never delete an invoice that's been + -- refunded (the credit notes are part of the customer's records), + -- but the schema should allow the admin tool to clean up for test + -- data. We drop and re-add the FKs with CASCADE so a delete tears + -- down everything related. The DROP/ADD pair is idempotent — on a + -- DB that already has the CASCADE variant it's a no-op (we drop + -- by name and re-add identically). + DO $cnfk$ + BEGIN + ALTER TABLE credit_notes + DROP CONSTRAINT IF EXISTS credit_notes_invoice_id_fkey; + ALTER TABLE credit_notes + ADD CONSTRAINT credit_notes_invoice_id_fkey + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE; + END + $cnfk$; + DO $irfk$ + BEGIN + ALTER TABLE invoice_refunds + DROP CONSTRAINT IF EXISTS invoice_refunds_invoice_id_fkey; + ALTER TABLE invoice_refunds + ADD CONSTRAINT invoice_refunds_invoice_id_fkey + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE; + END + $irfk$; + -- Invoice line items. The kind column lets the PDF renderer -- group lines (all monthly fees together, all AI usage together, -- etc.) and the admin UI filter by category. diff --git a/src/lib/pdf-brand.tsx b/src/lib/pdf-brand.tsx new file mode 100644 index 0000000..ae82757 --- /dev/null +++ b/src/lib/pdf-brand.tsx @@ -0,0 +1,126 @@ +/** + * Shared brand constants and Logo component for all PDF documents + * (invoices, credit notes, future quotes / reminders). + * + * Phase 7 fix: previously each PDF generator carried its own copy + * of BRAND and its own Logo. When Cedric customized the invoice + * issuer block in his deployment (real Strasse Nr., PLZ, etc.), + * the credit note PDF kept the original placeholders because it + * had its own duplicate. Hoisting both here means every PDF reads + * the same source of truth. + * + * To change the brand: edit BRAND below. To change the logo: + * edit Logo below. To change the issuer info Cedric ships: edit + * BRAND.issuer — both billing-pdf.tsx and credit-note-pdf.tsx pick + * it up automatically. + * + * The Logo component accepts a `color` prop so the credit-note + * variant can render the SAME shape tinted red (the document + * family is visually consistent; only the accent colour signals + * "this is a credit, not an invoice"). + */ + +import React from "react"; +import { Svg, Polygon, Polyline } from "@react-pdf/renderer"; + +// --------------------------------------------------------------------------- +// Brand constants +// --------------------------------------------------------------------------- + +export const BRAND = { + name: "PieCed IT", + // Primary emerald — matches the logo SVG fill (#10B981). + primary: "#10B981", + // Slightly darker emerald for headings. + primaryDark: "#0a8060", + textColor: "#1a1a1a", + mutedColor: "#666", + borderColor: "#d4d4d4", + // Issuer block — change these to your real legal info. + // Both billing-pdf.tsx and credit-note-pdf.tsx read from here. + issuer: { + legalName: "PieCed IT", + addressLine1: "Cedric Mosimann", + addressLine2: "[Strasse Nr.]", + postalCity: "[PLZ] Basel", + country: "Switzerland", + email: "billing@pieced.ch", + web: "pieced.ch", + // Show "MWST-Nr. ..." on PDF when set. + vatNumber: null as string | null, + // Bank instructions — used by invoice PDF, ignored on credit + // notes (refunds flow back via the original payment method). + bankName: "[Bank name]", + bankIban: "[CHxx xxxx xxxx xxxx xxxx x]", + bankBic: "[BIC]", + }, +}; + +// --------------------------------------------------------------------------- +// Accent colours for document variants — credit notes are red so +// customers can tell them apart from invoices at a glance. +// --------------------------------------------------------------------------- + +export const ACCENT_CREDIT_NOTE = { + primary: "#DC2626", + primaryDark: "#991B1B", +}; + +// --------------------------------------------------------------------------- +// Logo — PieCed's hexagon-pattern mark. Same shape used everywhere; +// only the colour changes per document type. +// --------------------------------------------------------------------------- + +interface LogoProps { + size?: number; + /** Defaults to BRAND.primary (emerald). Pass ACCENT_CREDIT_NOTE.primary + * on credit notes for the red variant. */ + color?: string; +} + +export const Logo = ({ size = 60, color = BRAND.primary }: LogoProps) => ( + + {/* H1 solid */} + + {/* H2 outline */} + + {/* H3 outline */} + + {/* H4 solid */} + + {/* H5 partial */} + + {/* H6 partial */} + + +); diff --git a/src/messages/de.json b/src/messages/de.json index 24c01e6..dd2a29a 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -699,7 +699,10 @@ "creditNotePdfHeader": "PDF", "creditNoteKind_void": "Storno", "creditNoteKind_refund": "Rückerstattung", - "creditNoteNoPdf": "—" + "creditNoteNoPdf": "—", + "refundAmountLabel": "Betrag", + "refundReasonLabel": "Grund", + "refundAmountInclVatHint": "inkl. MWST" }, "skillCostDialog": { "title": "Aktivierungskosten bestätigen", @@ -772,7 +775,9 @@ "paid": "Bezahlt", "overdue": "Überfällig", "void": "Storniert", - "uncollectible": "Uneinbringlich" + "uncollectible": "Uneinbringlich", + "partially_refunded": "Teilrückerstattung", + "fully_refunded": "Vollständig rückerstattet" }, "payWithCard": "Mit Karte bezahlen", "redirectingToStripe": "Weiterleitung…", diff --git a/src/messages/en.json b/src/messages/en.json index 17343ca..7e02780 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -699,7 +699,10 @@ "creditNotePdfHeader": "PDF", "creditNoteKind_void": "Void", "creditNoteKind_refund": "Refund", - "creditNoteNoPdf": "—" + "creditNoteNoPdf": "—", + "refundAmountLabel": "Amount", + "refundReasonLabel": "Reason", + "refundAmountInclVatHint": "incl. VAT" }, "skillCostDialog": { "title": "Confirm activation cost", @@ -772,7 +775,9 @@ "paid": "Paid", "overdue": "Overdue", "void": "Void", - "uncollectible": "Uncollectible" + "uncollectible": "Uncollectible", + "partially_refunded": "Partially refunded", + "fully_refunded": "Fully refunded" }, "payWithCard": "Pay with card", "redirectingToStripe": "Redirecting…", diff --git a/src/messages/fr.json b/src/messages/fr.json index 59de435..785dce4 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -699,7 +699,10 @@ "creditNotePdfHeader": "PDF", "creditNoteKind_void": "Annulation", "creditNoteKind_refund": "Remboursement", - "creditNoteNoPdf": "—" + "creditNoteNoPdf": "—", + "refundAmountLabel": "Montant", + "refundReasonLabel": "Motif", + "refundAmountInclVatHint": "TVA incluse" }, "skillCostDialog": { "title": "Confirmer le coût d'activation", @@ -772,7 +775,9 @@ "paid": "Payée", "overdue": "En retard", "void": "Annulée", - "uncollectible": "Irrécouvrable" + "uncollectible": "Irrécouvrable", + "partially_refunded": "Partiellement remboursée", + "fully_refunded": "Entièrement remboursée" }, "payWithCard": "Payer par carte", "redirectingToStripe": "Redirection…", diff --git a/src/messages/it.json b/src/messages/it.json index f315325..6e2b7ff 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -699,7 +699,10 @@ "creditNotePdfHeader": "PDF", "creditNoteKind_void": "Annullamento", "creditNoteKind_refund": "Rimborso", - "creditNoteNoPdf": "—" + "creditNoteNoPdf": "—", + "refundAmountLabel": "Importo", + "refundReasonLabel": "Motivo", + "refundAmountInclVatHint": "IVA inclusa" }, "skillCostDialog": { "title": "Conferma costi di attivazione", @@ -772,7 +775,9 @@ "paid": "Pagata", "overdue": "In ritardo", "void": "Annullata", - "uncollectible": "Inesigibile" + "uncollectible": "Inesigibile", + "partially_refunded": "Rimborsata parzialmente", + "fully_refunded": "Rimborsata integralmente" }, "payWithCard": "Paga con carta", "redirectingToStripe": "Reindirizzamento…",