Skip to content

[Fact 03] Factuur opstellen (concept) met handmatige regels #148

Description

@MiniMaxi-user

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).

Bestaande patronen om te spiegelen

  • Stal-overzichtspagina: src/app/(app)/stal/contracten/page.tsx (laadt
    getActiveStableId, behandelt ALLE_STALLEN, weert platform-admin/eigenaar af).
  • Server actions + validatie: src/features/contracten/actions.ts (getStableRole-
    check, leesNietNegatiefGetal-stijl validatie die een Error gooit, revalidatePath +
    redirect).
  • Formulier-component: src/features/contracten/ContractForm.tsx /
    ContractStepperForm.tsx (Nederlandse labels, form-group/input/btn-primary
    conventies, SubmitButton).
  • 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.

Scope

Binnen scope:

  • Concept-factuur aanmaken (route src/app/(app)/stal/facturen/nieuw/page.tsx):
    • Kies ontvanger (recipientUserId: een eigenaar/leaser; gevalideerd).
    • Optioneel een bron-contract koppelen (contractId, mag leeg blijven -- het
      voorvullen uit het contract is Fact 04, hier alleen de koppeling).
    • Factuurdatum en vervaldatum instelbaar (mogen in concept leeg blijven).
    • Vrije opmerking (notes).
    • Minimaal een regel vereist om een geldige concept-factuur op te slaan.
    • De factuur wordt opgeslagen met status = CONCEPT en zonder invoiceNumber
      (nummering is Fact 05).
  • Handmatige regels toevoegen / bewerken / verwijderen
    (src/app/(app)/stal/facturen/[id]/bewerken/page.tsx):
    • 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.

Technische notities

  • Routes (App Router, ingelogde omgeving):
    • src/app/(app)/stal/facturen/nieuw/page.tsx -- concept aanmaken.
    • src/app/(app)/stal/facturen/[id]/bewerken/page.tsx -- regels beheren + totalen.
    • 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.
    • werkFactuurkopBij -- ontvanger/contract/datums/notes bijwerken (alleen concept).
    • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions