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-modellen, enums InvoiceStatus/VatRate) en Fact 02
(#147: autorisatie- en inzage-laag in src/features/facturen/). Dit is de eerste
story met UI/acties: de bouwbare kern om een concept-factuur handmatig op te
stellen, voor het voorvullen uit contracten (Fact 04) en de PDF/nummering (Fact 05).
User Story
Als staleigenaar of stalmedewerker (OWNER/STAFF)
wil ik voor een ontvanger een concept-factuur kunnen opstellen met handmatig
ingevoerde regels (omschrijving, aantal, stuksprijs en btw-tarief per regel)
zodat ik -- nog voor de automatische voorvulling uit contracten -- een factuur kan
samenstellen en de totalen (excl. btw / btw / incl. btw) zie kloppen.
Context
Fact 01 (#146) legde het datamodel vast (Invoice, InvoiceLine, enums InvoiceStatus en
VatRate met NUL/LAAG/HOOG = 0/9/21%). Fact 02 (#147) leverde de server-side
autorisatie- en inzage-laag in src/features/facturen/ (queries.ts, authorization.ts,
facturenStorage.ts) -- o.a. assertCanManageInvoicesForStable, assertCanManageInvoice,
getInvoicesForActiveStable/getInvoicesForStable en getInvoiceForManagement.
Deze story bouwt daar de beheer-UI + server actions bovenop: het handmatig
opstellen en bewerken van een concept-factuur. Het btw-tarief staat per regel (zoals
in het datamodel); de totalen worden in de app-laag berekend met Decimal en
gedenormaliseerd opgeslagen op de Invoice (subtotal, vatAmount, total).
Conform CLAUDE.md: Nederlandse UI, design-tokens uit src/styles/globals.css,
autorisatie als kernlogica server-side, mutaties via server actions. We spiegelen de
bestaande CRUD-patronen van de contract-/paarden-modules (formulier-component +
server actions met revalidatePath/redirect, server-side validatie die bij overtreding
een Error gooit, en de stal-overzichtspagina onder
src/app/(app)/stal/contracten/page.tsx).
Ontvanger kiezen: zoals counterpartyUserId in de contract-actions (gevalideerd
tegen de relatie eigenaar-paard), hier recipientUserId voor de eigenaar/leaser.
Beheer-guards/queries: src/features/facturen/authorization.ts + queries.ts
(Fact 02) hergebruiken; niet dupliceren.
Duplicatie & journey
Er bestaat nog geen "Facturen"-scherm of -navigatie-item (gecontroleerd:
SidebarClient.tsx heeft Dashboard, Paarden, Lease, Contracten, Team, Accounts, Taken,
Instellingen -- geen Facturen). Het volledige facturen-overzicht is bewust Fact
07. Om versnippering te voorkomen kiest deze story bewust voor de route-prefix
/stal/facturen (analoog aan /stal/contracten), zodat Fact 07 daar later het overzicht
aan toevoegt zonder een concurrerend tweede ingangspunt. Deze story voegt geen
sidebar-navigatie-item toe (dat hoort bij het overzicht, Fact 07); de concept-aanmaak/
bewerk-pagina's zijn in deze story bereikbaar via directe route.
Per regel: omschrijving (description, verplicht), aantal (quantity,
groter dan 0), stuksprijs excl. btw (unitPrice, groter/gelijk aan 0), btw-tarief (vatRate: 0% / 9% / 21% via VatRate).
Regelvolgorde wordt bewaard (position).
Regel verwijderen; bij verwijderen van de laatste regel: opslaan geweigerd
(minimaal een regel).
Totaalberekening (server-side, Decimal):
lineTotal per regel = quantity x unitPrice, afgerond op 2 decimalen.
subtotal = som van alle lineTotal (excl. btw).
vatAmount = som van de btw per btw-tarief gegroepeerd (0/9/21% op het
subtotaal van dat tarief), afgerond op 2 decimalen.
total = subtotal + vatAmount.
Deze drie velden worden gedenormaliseerd op de Invoice opgeslagen en getoond.
Een btw-overzicht per tarief (grondslag + btw-bedrag) wordt in de UI getoond.
Bewerken alleen in concept: alle mutatie-acties weigeren server-side wanneer de
factuur niet (meer) CONCEPT is.
Autorisatie: alle pagina's en acties dwingen via de Fact 02-guards
(assertCanManageInvoicesForStable / assertCanManageInvoice) af dat de gebruiker
OWNER/STAFF van de uitgevende stal is.
Berekenlogica als herbruikbare helper in src/features/facturen/ (bv.
berekeningen.ts met Decimal), zodat Fact 04/05 dezelfde optelling hergebruiken.
Buiten scope (latere Fact-stories):
Regels voorvullen uit het contract (stalling + lease, incl. btw) -- Fact 04.
Factuur-PDF, huisstijl, opvolgende factuurnummering, btw-overzicht op PDF --
Fact 05. Deze story kent geen invoiceNumber toe.
Betaalwijze & SEPA-incassomachtiging -- Fact 06.
Status-overgangen (CONCEPT -> VERZONDEN/BETAALD/...), verzenden, herinneringen en het facturatie-overzicht + sidebar-navigatie -- Fact 07.
Wijzigingen aan het Prisma-schema (datamodel is compleet via Fact 01) en aan de
Fact 02-autorisatie-laag (wordt hergebruikt, niet gewijzigd).
Eigenaar/leaser-weergave van facturen (read-only inzage = Fact 07; query bestaat al
via Fact 02 getInvoicesForRecipient).
Acceptatiecriteria
Als een OWNER/STAFF op /stal/facturen/nieuw een ontvanger kiest en het
formulier opslaat, dan wordt een Invoice met status = CONCEPT, zonder
invoiceNumber, voor de actieve stal aangemaakt en kom ik op de bewerk-pagina.
Als ik op de bewerk-pagina een regel toevoeg (omschrijving, aantal,
stuksprijs, btw-tarief), dan wordt deze opgeslagen met de juiste position en
herberekent de factuur subtotal, vatAmount en total.
Als ik een bestaande regel wijzig of verwijder, dan worden de regel en
de totalen overeenkomstig bijgewerkt.
Als een concept-factuur 0 regels zou overhouden, dan wordt
opslaan/verwijderen geweigerd met een nette Nederlandse melding (minimaal een
regel).
De totalen kloppen en de btw wordt per tarief (0/9/21%) gegroepeerd berekend
en getoond; lineTotal = quantity x unitPrice, alle bedragen op 2 decimalen via
Decimal (geen floating-point afrondfouten).
Als een verplicht veld ontbreekt of ongeldig is (geen ontvanger, lege
omschrijving, aantal kleiner/gelijk 0, negatieve prijs, onbekend btw-tarief), dan wordt opslaan server-side geweigerd met een duidelijke melding.
Als een gebruiker zonder OWNER/STAFF-rol op de uitgevende stal een
factuur-pagina of -actie aanroept, dan wordt deze server-side geweigerd
("Geen toegang").
Als de factuur niet (meer) CONCEPT is, dan weigeren alle mutatie-acties
server-side (bewerken alleen in concept).
UI is Nederlandstalig en gebruikt de bestaande design-tokens/CSS-klassen
(form-group, input, btn-primary, page-container, etc.); er is geen
tailwind.config.ts of nieuwe kleur/token geintroduceerd.
npx tsc --noEmit slaagt; de berekenlogica zit in een herbruikbare helper in
src/features/facturen/; geen schema-wijzigingen.
Beide spiegelen stal/contracten/page.tsx: getAuthUser(), platform-admin ->
/admin, eigenaar zonder stalrol -> /eigenaar, actieve stal via getActiveStableId.
Server actions (src/features/facturen/actions.ts, met de use-server-directive):
maakConceptFactuur(formData) -- valideert ontvanger + minimaal 1 regel, dwingt
assertCanManageInvoicesForStable(userId, activeStableId) af, maakt Invoice +
InvoiceLines in een prisma.$transaction, berekent totalen, redirect naar de
bewerk-pagina.
voegFactuurregelToe / werkFactuurregelBij / verwijderFactuurregel -- elk via
assertCanManageInvoice(userId, invoiceId), weigeren wanneer status != CONCEPT,
herberekenen daarna de totalen en revalidatePath.
Validatie in de stijl van leesNietNegatiefGetal uit de contract-actions (gooit
Error bij negatief/onleesbaar/ontbrekend).
Decimal-rekenen: gebruik Prisma.Decimal voor alle bedragen; reken lineTotal, btw
per tarief en totalen met Decimal en rond af op 2 decimalen (toDecimalPlaces(2)).
Plaats dit in een aparte, test-vriendelijke helper, bv.
src/features/facturen/berekeningen.ts, zodat Fact 04/05 hem hergebruiken. Het
btw-percentage per VatRate (NUL=0, LAAG=9, HOOG=21) als centrale mapping in
src/features/facturen/ (label + percentage), niet inline.
Ontvanger- en contractkeuze: lever de keuzelijsten via een query in
src/features/facturen/queries.ts (eigenaren/leasers van de stal; contracten van de
stal), gescoped op de actieve stal. Spiegel hoe de contract-flow counterpartyUserId
selecteert en valideert.
Hergebruik Fact 02: importeer guards/queries uit src/features/facturen/; voeg
geen tweede autorisatiepad toe.
Formulier-UX: een client-component voor de regels (toevoegen/bewerken/
verwijderen met live subtotaal/btw/totaal-indicatie) mag, maar de bron van
waarheid blijft de server -- de definitieve berekening en validatie gebeurt
server-side bij opslaan (geen localStorage voor kernlogica, CLAUDE.md).
Open vragen
Geen blokkerende open vragen. Onderbouwde keuzes hieronder zijn als voorstel verwerkt;
de bouwer mag bij sterke voorkeur afwijken zolang de acceptatiecriteria gehaald worden:
Route-prefix /stal/facturen (i.p.v. los /facturen): gekozen voor consistentie
met /stal/contracten en om Fact 07 (overzicht + navigatie) een natuurlijke plek te
geven zonder versnippering.
Geen sidebar-item in deze story: het navigatie-item "Facturen" hoort bij het
overzicht (Fact 07); deze story levert de concept-aanmaak/bewerk-flow.
Twee-staps-flow (eerst kop aanmaken, dan regels op de bewerk-pagina) i.p.v. een
groot formulier: sluit aan op het Invoice/InvoiceLine-datamodel en het bestaande
"aanmaken -> detail/bewerken"-patroon van de contract-module, en houdt regelbeheer
(toevoegen/bewerken/verwijderen) eenvoudig en transactioneel.
User Story
Als staleigenaar of stalmedewerker (OWNER/STAFF)
wil ik voor een ontvanger een concept-factuur kunnen opstellen met handmatig
ingevoerde regels (omschrijving, aantal, stuksprijs en btw-tarief per regel)
zodat ik -- nog voor de automatische voorvulling uit contracten -- een factuur kan
samenstellen en de totalen (excl. btw / btw / incl. btw) zie kloppen.
Context
Fact 01 (#146) legde het datamodel vast (Invoice, InvoiceLine, enums InvoiceStatus en
VatRate met NUL/LAAG/HOOG = 0/9/21%). Fact 02 (#147) leverde de server-side
autorisatie- en inzage-laag in src/features/facturen/ (queries.ts, authorization.ts,
facturenStorage.ts) -- o.a. assertCanManageInvoicesForStable, assertCanManageInvoice,
getInvoicesForActiveStable/getInvoicesForStable en getInvoiceForManagement.
Deze story bouwt daar de beheer-UI + server actions bovenop: het handmatig
opstellen en bewerken van een concept-factuur. Het btw-tarief staat per regel (zoals
in het datamodel); de totalen worden in de app-laag berekend met Decimal en
gedenormaliseerd opgeslagen op de Invoice (subtotal, vatAmount, total).
Conform CLAUDE.md: Nederlandse UI, design-tokens uit src/styles/globals.css,
autorisatie als kernlogica server-side, mutaties via server actions. We spiegelen de
bestaande CRUD-patronen van de contract-/paarden-modules (formulier-component +
server actions met revalidatePath/redirect, server-side validatie die bij overtreding
een Error gooit, en de stal-overzichtspagina onder
src/app/(app)/stal/contracten/page.tsx).
Bestaande patronen om te spiegelen
getActiveStableId, behandelt ALLE_STALLEN, weert platform-admin/eigenaar af).
check, leesNietNegatiefGetal-stijl validatie die een Error gooit, revalidatePath +
redirect).
ContractStepperForm.tsx (Nederlandse labels, form-group/input/btn-primary
conventies, SubmitButton).
tegen de relatie eigenaar-paard), hier recipientUserId voor de eigenaar/leaser.
(Fact 02) hergebruiken; niet dupliceren.
Duplicatie & journey
Er bestaat nog geen "Facturen"-scherm of -navigatie-item (gecontroleerd:
SidebarClient.tsx heeft Dashboard, Paarden, Lease, Contracten, Team, Accounts, Taken,
Instellingen -- geen Facturen). Het volledige facturen-overzicht is bewust Fact
07. Om versnippering te voorkomen kiest deze story bewust voor de route-prefix
/stal/facturen (analoog aan /stal/contracten), zodat Fact 07 daar later het overzicht
aan toevoegt zonder een concurrerend tweede ingangspunt. Deze story voegt geen
sidebar-navigatie-item toe (dat hoort bij het overzicht, Fact 07); de concept-aanmaak/
bewerk-pagina's zijn in deze story bereikbaar via directe route.
Scope
Binnen scope:
voorvullen uit het contract is Fact 04, hier alleen de koppeling).
(nummering is Fact 05).
(src/app/(app)/stal/facturen/[id]/bewerken/page.tsx):
groter dan 0), stuksprijs excl. btw (unitPrice, groter/gelijk aan 0),
btw-tarief (vatRate: 0% / 9% / 21% via VatRate).
(minimaal een regel).
subtotaal van dat tarief), afgerond op 2 decimalen.
factuur niet (meer) CONCEPT is.
(assertCanManageInvoicesForStable / assertCanManageInvoice) af dat de gebruiker
OWNER/STAFF van de uitgevende stal is.
berekeningen.ts met Decimal), zodat Fact 04/05 dezelfde optelling hergebruiken.
Buiten scope (latere Fact-stories):
Fact 05. Deze story kent geen invoiceNumber toe.
herinneringen en het facturatie-overzicht + sidebar-navigatie -- Fact 07.
Fact 02-autorisatie-laag (wordt hergebruikt, niet gewijzigd).
via Fact 02 getInvoicesForRecipient).
Acceptatiecriteria
formulier opslaat, dan wordt een Invoice met status = CONCEPT, zonder
invoiceNumber, voor de actieve stal aangemaakt en kom ik op de bewerk-pagina.
stuksprijs, btw-tarief), dan wordt deze opgeslagen met de juiste position en
herberekent de factuur subtotal, vatAmount en total.
de totalen overeenkomstig bijgewerkt.
opslaan/verwijderen geweigerd met een nette Nederlandse melding (minimaal een
regel).
en getoond; lineTotal = quantity x unitPrice, alle bedragen op 2 decimalen via
Decimal (geen floating-point afrondfouten).
omschrijving, aantal kleiner/gelijk 0, negatieve prijs, onbekend btw-tarief),
dan wordt opslaan server-side geweigerd met een duidelijke melding.
factuur-pagina of -actie aanroept, dan wordt deze server-side geweigerd
("Geen toegang").
server-side (bewerken alleen in concept).
(form-group, input, btn-primary, page-container, etc.); er is geen
tailwind.config.ts of nieuwe kleur/token geintroduceerd.
src/features/facturen/; geen schema-wijzigingen.
Technische notities
/admin, eigenaar zonder stalrol -> /eigenaar, actieve stal via getActiveStableId.
assertCanManageInvoicesForStable(userId, activeStableId) af, maakt Invoice +
InvoiceLines in een prisma.$transaction, berekent totalen, redirect naar de
bewerk-pagina.
assertCanManageInvoice(userId, invoiceId), weigeren wanneer status != CONCEPT,
herberekenen daarna de totalen en revalidatePath.
Error bij negatief/onleesbaar/ontbrekend).
per tarief en totalen met Decimal en rond af op 2 decimalen (toDecimalPlaces(2)).
Plaats dit in een aparte, test-vriendelijke helper, bv.
src/features/facturen/berekeningen.ts, zodat Fact 04/05 hem hergebruiken. Het
btw-percentage per VatRate (NUL=0, LAAG=9, HOOG=21) als centrale mapping in
src/features/facturen/ (label + percentage), niet inline.
src/features/facturen/queries.ts (eigenaren/leasers van de stal; contracten van de
stal), gescoped op de actieve stal. Spiegel hoe de contract-flow counterpartyUserId
selecteert en valideert.
geen tweede autorisatiepad toe.
verwijderen met live subtotaal/btw/totaal-indicatie) mag, maar de bron van
waarheid blijft de server -- de definitieve berekening en validatie gebeurt
server-side bij opslaan (geen localStorage voor kernlogica, CLAUDE.md).
Open vragen
Geen blokkerende open vragen. Onderbouwde keuzes hieronder zijn als voorstel verwerkt;
de bouwer mag bij sterke voorkeur afwijken zolang de acceptatiecriteria gehaald worden:
met /stal/contracten en om Fact 07 (overzicht + navigatie) een natuurlijke plek te
geven zonder versnippering.
overzicht (Fact 07); deze story levert de concept-aanmaak/bewerk-flow.
groot formulier: sluit aan op het Invoice/InvoiceLine-datamodel en het bestaande
"aanmaken -> detail/bewerken"-patroon van de contract-module, en houdt regelbeheer
(toevoegen/bewerken/verwijderen) eenvoudig en transactioneel.