You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Onderdeel van epic #145 (Facturatie-module). Bouwt voort op Fact 01 (#146: Invoice/InvoiceLine + enums InvoiceStatus/VatRate, invoiceNumber String? @unique, invoiceDate DateTime?), Fact 02 (#147: autorisatie/inzage + getSignedUrlVoorFactuur
en getSignedUrlVoorFactuurPdf, bucket-constante FACTUUR_PDF_BUCKET), Fact 03 (#148:
concept-factuur + berekeningen.ts met berekenFactuurTotalen/btw per tarief) en Fact 04
(#149: regels voorvullen uit contract). Spiegelt bewust de bestaande contract-PDF-stack.
User Story
Als staleigenaar of stalmedewerker (OWNER/STAFF)
wil ik een concept-factuur definitief kunnen maken tot een nette PDF in huisstijl, met een
uniek, opvolgend factuurnummer (per stal, per jaargebonden reeks) en een btw-overzicht per
tarief
zodat ik de factuur kan uitreiken aan de ontvanger, het nummer voldoet aan de
boekhoud-/btw-eisen (lopende reeks zonder gaten) en de ontvanger de PDF kan inzien via dezelfde
afgeschermde signed-URL-route als bij Fact 02.
Context
Fact 03/04 leveren een concept-factuur met (handmatige of uit het contract voorgevulde) InvoiceLine-regels en server-side berekende, gedenormaliseerde totalen (subtotal, vatAmount, total) met btw per tarief gegroepeerd (berekenFactuurTotalen, btw-mapping NUL/LAAG/HOOG = 0/9/21%). Een concept heeft bewust nog geeninvoiceNumber en geen PDF.
Deze story sluit de keten opstellen tot definitief maken af. Bij het definitief maken wordt:
de factuurdatum (invoiceDate) vastgezet wanneer die nog leeg is;
een PDF in huisstijl gegenereerd met afzender (stal + logo), ontvanger, regels en een btw-overzicht per tarief, en opgeslagen in Supabase Storage;
de PDF gekoppeld zodat inzage via signed URL loopt langs de bestaande Fact 02-helpers
(getSignedUrlVoorFactuur / getSignedUrlVoorFactuurPdf).
De contract-PDF-stack is al vastgelegd en moet exact als blauwdruk dienen (zie project memory en src/features/contracten/pdf.ts): @react-pdf/renderer met renderToBuffer; een *PdfDocument.tsx-component in huisstijl; een pure data-bouwer *Data.ts; een prive
Supabase-bucket met inzage uitsluitend via signed URL (korte TTL); en een document-koppelmodel met storagePath zoals ContractDocument. Geen headless browser of nieuwe PDF-lib.
Fact 02 heeft de inzage-helper al klaargezet: getSignedUrlVoorFactuurPdf(storagePath) en getSignedUrlVoorFactuur(userId, invoiceId, storagePath) nemen het storagePath als parameter
(Fact 02 gokte bewust geen opslagmodel). Deze story vult dat in: het persisteren van het pad en
het doorgeven ervan aan de Fact 02-helper.
Scope
Binnen scope:
Opslagmodel voor de factuur-PDF (Prisma + migratie), spiegelt ContractDocument. Voorstel:
een nieuw model InvoiceDocument (invoiceId naar Invoice, onDelete: Cascade; storagePath String; createdAt; index op invoiceId) + back-relation documents InvoiceDocument[] op Invoice. Zo blijft de inzage gelijk aan de contract-stack
(laatste document wint) en sluit het aan op de bestaande, parameter-gestuurde Fact 02-helper.
(Schemawijziging mag zonder vooraf overleg, zie project memory.)
Jaargebonden, race-safe nummerreeks (Prisma + migratie), een teller-model. Voorstel: InvoiceNumberSequence met @@unique([stableId, year]) en een lastNumber Int. Het ophogen
gebeurt atomair binnen dezelfde prisma.$transaction als het toekennen van het
factuurnummer, zodat parallelle definitief-maak-acties geen dubbel/overgeslagen nummer kunnen
produceren. De DB-@unique op Invoice.invoiceNumber is het laatste vangnet.
Factuurnummer-formaat, een leesbaar, sorteerbaar, jaargebonden nummer per stal. Voorstel: YYYY-NNNN (bv. 2026-0001), met de teller die per stal per jaar bij 1 begint. Het nummer
wordt als string in Invoice.invoiceNumber opgeslagen (bestaand veld).
Server-action "factuur definitief maken" in src/features/facturen/actions.ts (bv. maakFactuurDefinitief(invoiceId)):
dwingt assertCanManageInvoice(userId, invoiceId) af (Fact 02);
weigert wanneer de factuur niet (meer) CONCEPT is (geen dubbel nummeren) en wanneer er 0 regels zijn (een lege factuur kan niet definitief);
kent binnen een $transaction het volgende nummer toe (teller ophogen + schrijven), zet invoiceDate (indien leeg) op vandaag en zet de status naar VERZONDEN (zie open punt);
genereert daarna de PDF, slaat die op en doet revalidatePath.
PDF-generatie + opslag in nieuwe src/features/facturen/-bestanden (bv. pdf.ts + FactuurPdfDocument.tsx + factuurPdfData.ts), gemodelleerd naar de contract-stack:
renderFactuurPdfBuffer(...) rendert in-memory via @react-pdf/renderer;
genereerEnSlaFactuurPdfOp(invoiceId) schrijft de buffer naar FACTUUR_PDF_BUCKET (idempotente
bucket-provisioning, prive) en maakt een InvoiceDocument-rij met storagePath;
de PDF toont: afzender (stalnaam, -adres, logo via getStableLogoDataUrl), ontvanger
(factuurgegevens uit OwnerBusinessProfile, met val-terug op User.name/email en het reguliere
adres wanneer er geen afwijkend factuuradres is), factuurnummer, factuurdatum en vervaldatum, de regels (omschrijving, aantal, stuksprijs excl. btw, btw-tarief,
regelbedrag), een btw-overzicht per tarief (grondslag + btw-bedrag per 0/9/21%) en subtotaal / totale btw / totaal incl. btw, allemaal uit berekenFactuurTotalen (Fact 03).
Inzage via signed URL, koppel het opgeslagen storagePath aan de bestaande Fact 02-helper:
een query/wrapper die het laatsteInvoiceDocument.storagePath ophaalt en doorgeeft aan getSignedUrlVoorFactuur(userId, invoiceId, storagePath). De leesrechten blijven exact die van
Fact 02 (ontvanger: eigen + niet-CONCEPT; stalrol: eigen stal).
UI-trigger op de bestaande bewerk-route (/stal/facturen/[id]/bewerken): een Nederlandse knop
"Factuur definitief maken", alleen actief bij een CONCEPT-factuur met minimaal een regel; na
afloop het toegekende factuurnummer + een knop "PDF openen" (signed URL). Geen nieuw scherm en geen sidebar-navigatie-item (overzicht + navigatie = Fact 07).
Huisstijl & taal: Nederlandse teksten, design-tokens uit src/styles/globals.css (navy,
cream, goud), Cormorant Garamond + Inter, Velaro-logo als fallback, exact zoals de contract-PDF.
Geen tailwind.config.ts, geen nieuwe kleuren/fonts.
Buiten scope (latere/andere Fact-stories):
Betaalwijze & SEPA-incassomachtiging (IBAN/mandaat op de PDF), Fact 06.
Betaalstatus-overgangen, daadwerkelijk verzenden (e-mail/notificatie), herinneringen en het
facturatie-overzicht + sidebar-navigatie, Fact 07. Deze story zet status op VERZONDEN als
markering van definitief/uitgereikt, maar bouwt geen verzendkanaal of overzicht.
Wijzigingen aan de Fact 02-autorisatielaag (wordt hergebruikt) en aan OwnerBusinessProfile
(wordt uitgelezen, niet gewijzigd). Een KvK/btw-nummer van de stal toevoegen valt buiten scope
(zie open punt).
Creditfacturen, factuur-PDF-versiebeheer, herfacturatie of het wijzigen van een reeds
definitieve factuur. Een definitieve factuur is read-only (mutatie-acties uit Fact 03 weigeren
al buiten CONCEPT).
Als een OWNER/STAFF een CONCEPT-factuur met minimaal een regel definitief maakt, dan
krijgt de factuur een uniek, opvolgend invoiceNumber, wordt invoiceDate (indien leeg) op
vandaag gezet en wordt de status VERZONDEN.
Als een concept-factuur 0 regels heeft of niet (meer) CONCEPT is, dan wordt
het definitief maken server-side geweigerd met een nette Nederlandse melding (geen dubbel
nummeren).
De factuurnummers vormen per stal een lopende, jaargebonden reeks zonder gaten of
duplicaten (bv. 2026-0001, 2026-0002, ...; bij jaarwissel begint de reeks opnieuw bij
1). Concept-facturen krijgen nooit een nummer.
Als twee definitief-maak-acties voor dezelfde stal (nagenoeg) gelijktijdig lopen, dan
krijgt elke factuur een uniek nummer (race-safe: atomaire teller binnen een transactie; de @unique op Invoice.invoiceNumber is het DB-vangnet).
De gegenereerde PDF toont: afzender (stalnaam, -adres, stallogo of Velaro-fallback),
ontvanger (factuurgegevens uit OwnerBusinessProfile met val-terug), factuurnummer,
factuurdatum en vervaldatum, alle regels (omschrijving, aantal, stuksprijs excl. btw,
btw-tarief, regelbedrag) en de totalen.
De PDF bevat een btw-overzicht per tarief (grondslag + btw-bedrag per voorkomend tarief
0/9/21%) plus subtotaal (excl. btw), totale btw en totaal (incl. btw), exact gelijk aan subtotal/vatAmount/total uit berekenFactuurTotalen.
De PDF wordt opgeslagen in Supabase Storage (prive bucket FACTUUR_PDF_BUCKET,
idempotente provisioning) en gekoppeld via het opslagmodel (InvoiceDocument.storagePath).
Als inzage in de factuur-PDF wordt gevraagd, dan loopt dit via getSignedUrlVoorFactuur (Fact 02) op het opgeslagen storagePath: de autorisatie wordt eerst
afgedwongen (ontvanger: eigen + niet-CONCEPT; stalrol: eigen stal) en daarna een signed URL met
korte TTL gegenereerd; is er geen PDF, dan null.
De PDF gebruikt de huisstijl (navy/cream/goud-tokens, Cormorant + Inter) en is
Nederlandstalig; er is geen nieuwe PDF-lib en geen tailwind.config.ts geintroduceerd, de
contract-PDF-stack (@react-pdf/renderer, Supabase Storage, document-model) is gespiegeld.
npx prisma migrate dev draait schoon (alleen nieuwe modellen/relatievelden, bestaande data
ongemoeid); npx prisma generate en npx tsc --noEmit slagen; de PDF-/nummerlogica zit in src/features/facturen/.
Technische notities
Spiegel de contract-PDF-stack (src/features/contracten/pdf.ts, ContractPdfDocument.tsx, pdfData.ts; pdfMerge.ts is niet nodig, facturen worden niet met bijlagen samengevoegd): renderToBuffer(createElement(FactuurPdfDocument, { data })) als pure render; bouwFactuurPdfData
als IO-vrije, test-vriendelijke data-bouwer; ensureFactuurPdfBucket idempotent
(getBucket/createBucket({ public: false }), race op already-exists negeren) via createAdminClient(); stallogo via getStableLogoDataUrl(stableId), null geeft Velaro-logo.
Bedragen/btw uit de bestaande helper: gebruik berekenFactuurTotalen (Fact 03, berekeningen.ts) voor regels, btwGroepen, subtotal, vatAmount, total; gebruik formatEuro en VAT_RATE_LABEL/VAT_RATE_PERCENTAGE voor de weergave. Niet opnieuw met floats
rekenen (CLAUDE.md / Fact 01-conventie).
Race-safe nummering, voorgestelde aanpak binnen een prisma.$transaction: upsert op InvoiceNumberSequence met key (stableId, year); bij bestaan lastNumber: { increment: 1 }, bij
ontbreken lastNumber: 1; de geretourneerde lastNumber is het volgnummer, geformatteerd als YYYY-NNNN (padStart 4). Schrijf in dezelfde transactie invoiceNumber, invoiceDate en status = VERZONDEN. De @unique op Invoice.invoiceNumber blijft de harde DB-garantie; vang een
unieke-constraint-fout netjes af. (Alternatief: SELECT FOR UPDATE of een Postgres-sequence; de
upsert-increment is de eenvoudigste sluitende variant.) Genereer/sla de PDF op na de geslaagde
transactie (de PDF heeft het nummer nodig).
Velden (bestaand, Fact 01):Invoice.invoiceNumber String? @unique en invoiceDate DateTime?
zijn al aanwezig, deze story vult ze en voegt ze niet toe. dueDate wordt door Fact 03 gezet en
hier alleen getoond.
Inzage: geef getSignedUrlVoorFactuur het storagePath van het laatsteInvoiceDocument
(newest-first), net als getSignedUrlVoorContract het laatste ContractDocument pakt. Voeg
eventueel een dunne wrapper toe die het pad opzoekt en doorgeeft; dupliceer de autorisatie niet.
Geen nieuw scherm/navigatie: de actie en de PDF-link leven op de bestaande /stal/facturen/[id]/bewerken-route; het navigatie-item "Facturen" en het overzicht horen bij
Fact 07 (gecontroleerd: SidebarClient.tsx heeft nog geen Facturen-item en er is geen ander
factuurscherm, dus geen duplicatie).
Prisma CLI draait via npx prisma in C:\Claude\velaro en leest .env. Schemawijzigingen mogen
zonder vooraf overleg (project memory); houd het strikt binnen deze scope.
Open vragen
Geen blokkerende open vragen. De onderstaande keuzes zijn als onderbouwd voorstel verwerkt; de
bouwer mag bij sterke voorkeur afwijken zolang de acceptatiecriteria gehaald worden:
Status bij definitief maken = VERZONDEN. Het bord ([Facturatie] Epic: Facturatie-module #145/[Fact 01] Datamodel & migratie — facturatie-kern #146) zegt nummer vastzetten bij
verzenden, niet bij concept, maar Fact 07 bezit de bredere status-/verzendworkflow. Om geen
circulaire afhankelijkheid te creeren kent deze story het nummer toe bij definitief maken en
zet de status op VERZONDEN (de enige niet-CONCEPT status die uitgereikt representeert). Fact 07
bouwt het feitelijke verzendkanaal (e-mail/notificatie), betaalstatus (BETAALD/VERVALLEN) en
herinneringen hierop voort.
Factuurnummer-formaat YYYY-NNNN per stal per jaar. Leesbaar, sorteerbaar en gangbaar
(jaargebonden reeks). Een ander, consistent formaat mag, mits de reeks per stal lopend en uniek
blijft.
Opslagmodel InvoiceDocument (i.p.v. een pdfPath-kolom op Invoice). Gekozen om de
contract-stack exact te spiegelen en de bestaande, parameter-gestuurde Fact 02-inzage-helper te
voeden. Een enkel pdfPath String?-veld op Invoice is een eenvoudiger alternatief; beide
voldoen aan de criteria.
Afzender-KvK/btw-nummer ontbreekt op Stable.OwnerBusinessProfile (ontvanger) heeft
KvK/btw; het Stable-model (afzender) niet. De PDF toont daarom de beschikbare stalgegevens
(naam/adres/logo). Een KvK/btw-veld op de stal toevoegen is een aparte, kleine uitbreiding;
voorstel: niet in deze story afdwingen (buiten scope) zodat Fact 05 niet uitloopt, eventueel als
losse follow-up. Niet blokkerend voor de kern (nummering + PDF + btw-overzicht + inzage).
User Story
Als staleigenaar of stalmedewerker (OWNER/STAFF)
wil ik een concept-factuur definitief kunnen maken tot een nette PDF in huisstijl, met een
uniek, opvolgend factuurnummer (per stal, per jaargebonden reeks) en een btw-overzicht per
tarief
zodat ik de factuur kan uitreiken aan de ontvanger, het nummer voldoet aan de
boekhoud-/btw-eisen (lopende reeks zonder gaten) en de ontvanger de PDF kan inzien via dezelfde
afgeschermde signed-URL-route als bij Fact 02.
Context
Fact 03/04 leveren een concept-factuur met (handmatige of uit het contract voorgevulde)
InvoiceLine-regels en server-side berekende, gedenormaliseerde totalen (subtotal,vatAmount,total) met btw per tarief gegroepeerd (berekenFactuurTotalen, btw-mappingNUL/LAAG/HOOG = 0/9/21%). Een concept heeft bewust nog geeninvoiceNumberen geen PDF.Deze story sluit de keten opstellen tot definitief maken af. Bij het definitief maken wordt:
definitief maken, nooit op een concept, conform [Fact 01] Datamodel & migratie — facturatie-kern #146/[Facturatie] Epic: Facturatie-module #145);
invoiceDate) vastgezet wanneer die nog leeg is;btw-overzicht per tarief, en opgeslagen in Supabase Storage;
(
getSignedUrlVoorFactuur/getSignedUrlVoorFactuurPdf).De contract-PDF-stack is al vastgelegd en moet exact als blauwdruk dienen (zie project memory en
src/features/contracten/pdf.ts):@react-pdf/renderermet renderToBuffer; een*PdfDocument.tsx-component in huisstijl; een pure data-bouwer*Data.ts; een priveSupabase-bucket met inzage uitsluitend via signed URL (korte TTL); en een document-koppelmodel met
storagePathzoalsContractDocument. Geen headless browser of nieuwe PDF-lib.Fact 02 heeft de inzage-helper al klaargezet:
getSignedUrlVoorFactuurPdf(storagePath)engetSignedUrlVoorFactuur(userId, invoiceId, storagePath)nemen het storagePath als parameter(Fact 02 gokte bewust geen opslagmodel). Deze story vult dat in: het persisteren van het pad en
het doorgeven ervan aan de Fact 02-helper.
Scope
Binnen scope:
ContractDocument. Voorstel:een nieuw model
InvoiceDocument(invoiceIdnaarInvoice,onDelete: Cascade;storagePath String;createdAt; index opinvoiceId) + back-relationdocuments InvoiceDocument[]opInvoice. Zo blijft de inzage gelijk aan de contract-stack(laatste document wint) en sluit het aan op de bestaande, parameter-gestuurde Fact 02-helper.
(Schemawijziging mag zonder vooraf overleg, zie project memory.)
InvoiceNumberSequencemet@@unique([stableId, year])en eenlastNumber Int. Het ophogengebeurt atomair binnen dezelfde
prisma.$transactionals het toekennen van hetfactuurnummer, zodat parallelle definitief-maak-acties geen dubbel/overgeslagen nummer kunnen
produceren. De DB-
@uniqueopInvoice.invoiceNumberis het laatste vangnet.YYYY-NNNN(bv.2026-0001), met de teller die per stal per jaar bij 1 begint. Het nummerwordt als string in
Invoice.invoiceNumberopgeslagen (bestaand veld).src/features/facturen/actions.ts(bv.maakFactuurDefinitief(invoiceId)):assertCanManageInvoice(userId, invoiceId)af (Fact 02);CONCEPTis (geen dubbel nummeren) en wanneer er0 regels zijn (een lege factuur kan niet definitief);
$transactionhet volgende nummer toe (teller ophogen + schrijven), zetinvoiceDate(indien leeg) op vandaag en zet de status naarVERZONDEN(zie open punt);revalidatePath.src/features/facturen/-bestanden (bv.pdf.ts+FactuurPdfDocument.tsx+factuurPdfData.ts), gemodelleerd naar de contract-stack:renderFactuurPdfBuffer(...)rendert in-memory via@react-pdf/renderer;genereerEnSlaFactuurPdfOp(invoiceId)schrijft de buffer naarFACTUUR_PDF_BUCKET(idempotentebucket-provisioning, prive) en maakt een
InvoiceDocument-rij metstoragePath;getStableLogoDataUrl), ontvanger(factuurgegevens uit
OwnerBusinessProfile, met val-terug opUser.name/emailen het reguliereadres wanneer er geen afwijkend factuuradres is), factuurnummer, factuurdatum en
vervaldatum, de regels (omschrijving, aantal, stuksprijs excl. btw, btw-tarief,
regelbedrag), een btw-overzicht per tarief (grondslag + btw-bedrag per 0/9/21%) en
subtotaal / totale btw / totaal incl. btw, allemaal uit
berekenFactuurTotalen(Fact 03).storagePathaan de bestaande Fact 02-helper:een query/wrapper die het laatste
InvoiceDocument.storagePathophaalt en doorgeeft aangetSignedUrlVoorFactuur(userId, invoiceId, storagePath). De leesrechten blijven exact die vanFact 02 (ontvanger: eigen + niet-CONCEPT; stalrol: eigen stal).
/stal/facturen/[id]/bewerken): een Nederlandse knop"Factuur definitief maken", alleen actief bij een
CONCEPT-factuur met minimaal een regel; naafloop het toegekende factuurnummer + een knop "PDF openen" (signed URL). Geen nieuw scherm en
geen sidebar-navigatie-item (overzicht + navigatie = Fact 07).
src/styles/globals.css(navy,cream, goud), Cormorant Garamond + Inter, Velaro-logo als fallback, exact zoals de contract-PDF.
Geen
tailwind.config.ts, geen nieuwe kleuren/fonts.Buiten scope (latere/andere Fact-stories):
facturatie-overzicht + sidebar-navigatie, Fact 07. Deze story zet status op
VERZONDENalsmarkering van definitief/uitgereikt, maar bouwt geen verzendkanaal of overzicht.
berekenFactuurTotalen; geen nieuwe btw-regels.OwnerBusinessProfile(wordt uitgelezen, niet gewijzigd). Een KvK/btw-nummer van de stal toevoegen valt buiten scope
(zie open punt).
definitieve factuur. Een definitieve factuur is read-only (mutatie-acties uit Fact 03 weigeren
al buiten CONCEPT).
Acceptatiecriteria
CONCEPT-factuur met minimaal een regel definitief maakt, dankrijgt de factuur een uniek, opvolgend
invoiceNumber, wordtinvoiceDate(indien leeg) opvandaag gezet en wordt de status
VERZONDEN.CONCEPTis, dan wordthet definitief maken server-side geweigerd met een nette Nederlandse melding (geen dubbel
nummeren).
duplicaten (bv.
2026-0001,2026-0002, ...; bij jaarwissel begint de reeks opnieuw bij1). Concept-facturen krijgen nooit een nummer.
krijgt elke factuur een uniek nummer (race-safe: atomaire teller binnen een transactie; de
@uniqueopInvoice.invoiceNumberis het DB-vangnet).ontvanger (factuurgegevens uit
OwnerBusinessProfilemet val-terug), factuurnummer,factuurdatum en vervaldatum, alle regels (omschrijving, aantal, stuksprijs excl. btw,
btw-tarief, regelbedrag) en de totalen.
0/9/21%) plus subtotaal (excl. btw), totale btw en totaal (incl. btw), exact gelijk aan
subtotal/vatAmount/totaluitberekenFactuurTotalen.FACTUUR_PDF_BUCKET,idempotente provisioning) en gekoppeld via het opslagmodel (
InvoiceDocument.storagePath).getSignedUrlVoorFactuur(Fact 02) op het opgeslagenstoragePath: de autorisatie wordt eerstafgedwongen (ontvanger: eigen + niet-CONCEPT; stalrol: eigen stal) en daarna een signed URL met
korte TTL gegenereerd; is er geen PDF, dan null.
Nederlandstalig; er is geen nieuwe PDF-lib en geen
tailwind.config.tsgeintroduceerd, decontract-PDF-stack (
@react-pdf/renderer, Supabase Storage, document-model) is gespiegeld.npx prisma migrate devdraait schoon (alleen nieuwe modellen/relatievelden, bestaande dataongemoeid);
npx prisma generateennpx tsc --noEmitslagen; de PDF-/nummerlogica zit insrc/features/facturen/.Technische notities
src/features/contracten/pdf.ts,ContractPdfDocument.tsx,pdfData.ts;pdfMerge.tsis niet nodig, facturen worden niet met bijlagen samengevoegd):renderToBuffer(createElement(FactuurPdfDocument, { data }))als pure render;bouwFactuurPdfDataals IO-vrije, test-vriendelijke data-bouwer;
ensureFactuurPdfBucketidempotent(
getBucket/createBucket({ public: false }), race op already-exists negeren) viacreateAdminClient(); stallogo viagetStableLogoDataUrl(stableId), null geeft Velaro-logo.berekenFactuurTotalen(Fact 03,berekeningen.ts) voor regels,btwGroepen,subtotal,vatAmount,total; gebruikformatEuroenVAT_RATE_LABEL/VAT_RATE_PERCENTAGEvoor de weergave. Niet opnieuw met floatsrekenen (CLAUDE.md / Fact 01-conventie).
prisma.$transaction:upsertopInvoiceNumberSequencemet key(stableId, year); bij bestaanlastNumber: { increment: 1 }, bijontbreken
lastNumber: 1; de geretourneerdelastNumberis het volgnummer, geformatteerd alsYYYY-NNNN(padStart 4). Schrijf in dezelfde transactieinvoiceNumber,invoiceDateenstatus = VERZONDEN. De@uniqueopInvoice.invoiceNumberblijft de harde DB-garantie; vang eenunieke-constraint-fout netjes af. (Alternatief: SELECT FOR UPDATE of een Postgres-sequence; de
upsert-increment is de eenvoudigste sluitende variant.) Genereer/sla de PDF op na de geslaagde
transactie (de PDF heeft het nummer nodig).
Invoice.invoiceNumber String? @uniqueeninvoiceDate DateTime?zijn al aanwezig, deze story vult ze en voegt ze niet toe.
dueDatewordt door Fact 03 gezet enhier alleen getoond.
getSignedUrlVoorFactuurhetstoragePathvan het laatsteInvoiceDocument(newest-first), net als
getSignedUrlVoorContracthet laatsteContractDocumentpakt. Voegeventueel een dunne wrapper toe die het pad opzoekt en doorgeeft; dupliceer de autorisatie niet.
/stal/facturen/[id]/bewerken-route; het navigatie-item "Facturen" en het overzicht horen bijFact 07 (gecontroleerd:
SidebarClient.tsxheeft nog geen Facturen-item en er is geen anderfactuurscherm, dus geen duplicatie).
npx prismainC:\Claude\velaroen leest.env. Schemawijzigingen mogenzonder vooraf overleg (project memory); houd het strikt binnen deze scope.
Open vragen
Geen blokkerende open vragen. De onderstaande keuzes zijn als onderbouwd voorstel verwerkt; de
bouwer mag bij sterke voorkeur afwijken zolang de acceptatiecriteria gehaald worden:
VERZONDEN. Het bord ([Facturatie] Epic: Facturatie-module #145/[Fact 01] Datamodel & migratie — facturatie-kern #146) zegt nummer vastzetten bijverzenden, niet bij concept, maar Fact 07 bezit de bredere status-/verzendworkflow. Om geen
circulaire afhankelijkheid te creeren kent deze story het nummer toe bij definitief maken en
zet de status op
VERZONDEN(de enige niet-CONCEPT status die uitgereikt representeert). Fact 07bouwt het feitelijke verzendkanaal (e-mail/notificatie), betaalstatus (
BETAALD/VERVALLEN) enherinneringen hierop voort.
YYYY-NNNNper stal per jaar. Leesbaar, sorteerbaar en gangbaar(jaargebonden reeks). Een ander, consistent formaat mag, mits de reeks per stal lopend en uniek
blijft.
InvoiceDocument(i.p.v. eenpdfPath-kolom opInvoice). Gekozen om decontract-stack exact te spiegelen en de bestaande, parameter-gestuurde Fact 02-inzage-helper te
voeden. Een enkel
pdfPath String?-veld opInvoiceis een eenvoudiger alternatief; beidevoldoen aan de criteria.
Stable.OwnerBusinessProfile(ontvanger) heeftKvK/btw; het
Stable-model (afzender) niet. De PDF toont daarom de beschikbare stalgegevens(naam/adres/logo). Een KvK/btw-veld op de stal toevoegen is een aparte, kleine uitbreiding;
voorstel: niet in deze story afdwingen (buiten scope) zodat Fact 05 niet uitloopt, eventueel als
losse follow-up. Niet blokkerend voor de kern (nummering + PDF + btw-overzicht + inzage).