Skip to content

[Fact 04] Factuurregels voorvullen uit het contract (stalling + lease) #149

Description

@MiniMaxi-user

Onderdeel van epic #145 (Facturatie-module). Bouwt voort op Fact 01 (#146:
Invoice/InvoiceLine + enums InvoiceStatus/VatRate), Fact 02 (#147:
autorisatie- en inzage-laag in src/features/facturen/) en Fact 03 (#148:
concept-factuur + handmatige regels - actions.ts, berekeningen.ts,
FactuurRegelsBeheer.tsx, routes /stal/facturen/nieuw + [id]/bewerken).

User Story

Als staleigenaar of stalmedewerker (OWNER/STAFF)
wil ik dat de factuurregels automatisch worden voorgevuld vanuit het aan de factuur
gekoppelde contract
- zowel een stallingscontract (maandelijkse pensionprijs + extra
diensten/prijslijst) als een leasecontract (leasevergoeding + leaser-deel van de
kostenverdeling) -
zodat ik een factuur niet handmatig hoef over te tikken, de juiste btw-tarieven per
regel meteen kloppen, en ik de voorgevulde regels daarna nog vrij kan bijstellen.

Context

Fact 03 (#148) levert de concept-factuur met handmatige regels: de twee-staps-flow
(kop aanmaken op /stal/facturen/nieuw, regels beheren op /stal/facturen/[id]/bewerken),
de server-side totaalberekening (berekeningen.ts -> berekenFactuurTotalen, btw per
tarief gegroepeerd, VatRate-mapping NUL/LAAG/HOOG = 0/9/21%) en de regel-acties
(voegFactuurregelToe / werkFactuurregelBij / verwijderFactuurregel). De factuurkop
kan al optioneel een bron-contract koppelen (contractId), maar dat doet op dit
moment niets met de regels - voorvullen is bewust deze story.

Deze story voegt een gerichte uitbreiding toe: regels voorvullen uit het gekoppelde
contract
, met de juiste btw-tarieven, als gewone bewerkbare InvoiceLine-regels. Het
zijn na het voorvullen identiek aan handmatige regels uit Fact 03 - ze worden via
dezelfde acties bewerkt/verwijderd en tellen mee in dezelfde totaalberekening. Er komt
geen apart "voorgevuld vs. handmatig"-onderscheid in het datamodel.

Twee contract-families bestaan (Contract.family, schema): STALLING en LEASE.
Beide bewaren hun inhoud als JSON op Contract.config. De bronvelden:

  • Stalling (config.prijsLooptijd + config.extraDiensten):
    • Pensionprijs: prijsLooptijd.prijs.bedrag (per maand), met btwModus
      (INCL/EXCL) en btwPercentage (instelbaar; doorgaans 21, of 9 voor het lage
      sport-/staltarief). Reader: leesPrijsLooptijd (src/features/contracten/prijsLooptijd.ts).
    • Extra diensten / prijslijst: extraDiensten.posten[] met omschrijving, bedrag,
      frequentie (EENMALIG/PER_MAAND/PER_JAAR/PER_KEER). Reader: leesExtraDiensten
      (src/features/contracten/bijlagenDiensten.ts).
  • Lease (config.lease, met daarin o.a. het kostenblok):
    • Leasevergoeding (excl. btw) + 21% btw + kostenverdeling per post (betaler
      EIGENAAR/LEASER). Readers: leesLeaseContractConfig
      (src/features/contracten/leaseContract.ts) -> veld kosten, en de bestaande
      rekenhelper berekenKosten + LEASE_BTW_TARIEF (src/features/lease/leaseKostenConfig.ts).
      Lease-btw is altijd 21%; het lage 9%-tarief geldt hier niet.

Scope

Binnen scope:

  • Voorvul-helper (puur, test-vriendelijk) in src/features/facturen/ - bv.
    contractVoorvullen.ts - die uit een ingelezen contract-config een lijst
    RuweFactuurregel[] (description, quantity, unitPrice, vatRate) afleidt:
    • Stalling:
      • Een regel "Stalling (pensionprijs)" uit prijsLooptijd.prijs.bedrag:
        quantity = 1, unitPrice = het bedrag excl. btw (bij btwModus = INCL wordt
        het bedrag teruggerekend naar excl. btw met btwPercentage), vatRate afgeleid uit
        btwPercentage (zie btw-mapping hieronder).
      • Per extra dienst (extraDiensten.posten[]) een regel met de omschrijving als
        description, het bedrag als unitPrice en quantity = 1. Btw-tarief: standaard
        gelijk aan dat van de pensionprijs (zelfde btwPercentage), tenzij anders gekozen
        (extra diensten kennen in het contract zelf geen apart btw-veld -> afgeleid van de
        pensionprijs; zie open punt). De frequentie wordt in de description vermeld
        (bv. "Boxservice (per maand)") zodat de stal weet waar de regel vandaan komt.
    • Lease:
      • Een regel "Leasevergoeding" uit het kostenblok: quantity = 1,
        unitPrice = de vergoeding excl. btw, vatRate = HOOG (21%).
      • Per kostenpost met betaler = LEASER (en een ingevuld bedrag) een regel met het
        postlabel (KOSTENPOSTEN-label, bv. "Hoefsmid") als description, het bedrag als
        unitPrice, quantity = 1, vatRate = HOOG (21%). Posten van de eigenaar
        worden niet voorgevuld (die factureert de stal niet aan de leaser).
  • Btw-mapping (stalling) - percentage -> VatRate: 0 -> NUL, 9 -> LAAG, 21 -> HOOG.
    Een afwijkend percentage (geen 0/9/21) wordt gemapt op het dichtstbijzijnde toegestane
    tarief en de regel blijft bewerkbaar (de enum kent enkel 0/9/21; zie open punt).
  • Btw-modus omrekenen (stalling): bij btwModus = INCL wordt unitPrice excl. btw =
    bedrag / (1 + btwPercentage/100), afgerond op 2 decimalen via de Decimal-helper. Bij
    EXCL is het bedrag al de excl.-btw-stuksprijs.
  • Server-action voorvulRegelsUitContract(invoiceId) in
    src/features/facturen/actions.ts:
    • Loopt via assertCanManageInvoice(userId, invoiceId) (Fact 02) en weigert wanneer de
      factuur niet (meer) CONCEPT is (assertConcept, Fact 03).
    • Vereist dat de factuur een contractId heeft; ontbreekt die, dan een nette NL-melding
      ("Koppel eerst een contract aan deze factuur."). Het contract moet bij dezelfde stal
      horen (isContractVanStable, hergebruik Fact 03).
    • Leest het contract, bepaalt de bronregels via de voorvul-helper op basis van
      Contract.family, voegt ze als InvoiceLine-regels toe achter de bestaande regels
      (oplopende position), in een prisma.$transaction, en herberekent + schrijft de
      totalen (herberekenEnSchrijfTotalen, Fact 03). revalidatePath op de bewerk-pagina.
    • Geen dubbeling: de actie voegt bij herhaald aanroepen niet dezelfde regels nogmaals
      toe. Voorgesteld: voorvullen is alleen toegestaan zolang de factuur uitsluitend
      de bij aanmaak verplichte regel(s) bevat dan wel nog niet eerder is voorgevuld - of de
      UI biedt het voorvullen alleen aan bij een "verse" concept-factuur. (Zie open punt;
      de bouwer kiest de eenvoudigste sluitende variant.)
  • UI op de bewerk-pagina (FactuurRegelsBeheer.tsx / de bewerk-route): een
    Nederlandse knop/actie "Regels voorvullen uit contract", alleen zichtbaar/actief
    wanneer er een bron-contract gekoppeld is en de factuur CONCEPT is. Voorgevulde regels
    verschijnen als gewone, bewerkbare regels (zelfde rij-UI als de handmatige regels).
  • Voorgevulde regels = handmatige regels: na voorvullen werken werkFactuurregelBij,
    verwijderFactuurregel en de totaalberekening er ongewijzigd op; er is geen apart
    regeltype, geen schema-wijziging.

Buiten scope (latere Fact-stories):

  • Factuur-PDF, huisstijl, opvolgende factuurnummering, btw-overzicht op de PDF -
    Fact 05. Deze story raakt geen nummering of PDF.
  • Betaalwijze & SEPA-incassomachtiging - Fact 06. De stalling-borg en betaalwijze
    worden hier niet voorgevuld.
  • Statusovergangen / verzenden / herinneringen / facturatie-overzicht + sidebar-
    navigatie - Fact 07.
  • Automatische periodieke facturatie-runs / proratie van deelmaanden / indexering -
    buiten de epic-scope (epic [Facturatie] Epic: Facturatie-module #145: eerst handmatig/triggerbaar). quantity blijft 1;
    deelperiodes/halve maanden corrigeert de stal handmatig in de regel.
  • Wijzigingen aan het Prisma-schema, aan de Fact 02-autorisatielaag of aan de
    contract-config-readers (alle worden hergebruikt, niet gewijzigd).

Acceptatiecriteria

  • Als een OWNER/STAFF een concept-factuur met een gekoppeld stallingscontract
    voorvult, dan verschijnt een regel "Stalling (pensionprijs)" met quantity = 1,
    de unitPrice excl. btw en het btw-tarief afgeleid uit het contract-
    btwPercentage (0/9/21% -> NUL/LAAG/HOOG).
  • Als het stallingscontract btwModus = INCL heeft, dan wordt de unitPrice
    correct teruggerekend naar excl. btw (bedrag / (1 + pct/100)), zodat
    regel-excl. + btw weer op het oorspronkelijke incl.-bedrag uitkomt (op 2 decimalen).
  • Als het stallingscontract extra diensten/prijslijst-posten heeft, dan wordt
    per post een regel voorgevuld (omschrijving + frequentie in de description, bedrag
    als unitPrice, quantity = 1, btw-tarief gelijk aan dat van de pensionprijs).
  • Als een concept-factuur met een gekoppeld leasecontract wordt voorgevuld,
    dan verschijnt een regel "Leasevergoeding" met de vergoeding excl. btw als
    unitPrice en vatRate = HOOG (21%).
  • Als het leasecontract kostenposten met betaler LEASER (en ingevuld bedrag)
    heeft, dan wordt per post een regel voorgevuld (postlabel als description,
    bedrag als unitPrice, vatRate = HOOG); posten van de eigenaar worden niet
    voorgevuld.
  • De voorgevulde regels zijn gewone bewerkbare InvoiceLine-regels: ze kunnen via
    de Fact 03-acties worden gewijzigd/verwijderd en tellen mee in subtotal/vatAmount/
    total, met btw per tarief gegroepeerd berekend (berekenFactuurTotalen).
  • Als er geen bron-contract aan de factuur gekoppeld is, dan is voorvullen niet
    mogelijk en krijgt de gebruiker een nette NL-melding.
  • Als de factuur niet (meer) CONCEPT is, dan weigert de voorvul-actie
    server-side (bewerken alleen in concept).
  • Als een gebruiker zonder OWNER/STAFF-rol op de uitgevende stal de voorvul-actie
    aanroept, dan wordt deze server-side geweigerd ("Geen toegang") via de Fact 02-guard.
  • Geen dubbeling: herhaald voorvullen voegt dezelfde contractregels niet nogmaals
    toe (de gekozen variant is sluitend en geeft anders een nette melding).
  • UI is Nederlandstalig en gebruikt de bestaande design-tokens/CSS-klassen; er is
    geen tailwind.config.ts of nieuw token geintroduceerd.
  • npx tsc --noEmit slaagt; de afleidlogica zit in een herbruikbare, test-vriendelijke
    helper in src/features/facturen/; geen schema-wijzigingen.

Technische notities

  • Hergebruik, niet dupliceren:
    • berekeningen.ts ([Fact 03] Factuur opstellen (concept) met handmatige regels #148): berekenFactuurTotalen, berekenLineTotal, de
      VatRate-mapping (VAT_RATE_PERCENTAGE/VAT_RATE_LABEL/VAT_RATES) en de
      Decimal-afronding. De voorvul-helper levert RuweFactuurregel[]; het optellen en
      de totalen blijven volledig in berekeningen.ts.
    • Stalling-config: leesPrijsLooptijd (prijsLooptijd.ts) en leesExtraDiensten
      (bijlagenDiensten.ts) - beide lezen defensief uit Contract.config.
    • Lease-config: leesLeaseContractConfig (leaseContract.ts) voor het kosten-blok,
      en berekenKosten + LEASE_BTW_TARIEF + KOSTENPOSTEN (leaseKostenConfig.ts)
      voor vergoeding/posten/labels. Lease-btw = 21% (HOOG); geen 9% bij lease.
    • Fact 02-guards/queries: assertCanManageInvoice, isContractVanStable; Fact 03:
      assertConcept, herberekenEnSchrijfTotalen, de leesRegelForm-stijl-validatie.
  • Btw-omrekening (stalling) met Prisma.Decimal en toDecimalPlaces(2) (zoals in
    berekeningen.ts); reken nooit met floats voor bedragen (CLAUDE.md / Fact 01-conventie).
  • Family-switch: kies de bronregels op Contract.family (STALLING vs LEASE).
    Onbekende/lege config valt via de defensieve readers terug op lege defaults -> lege
    regellijst, geen crash.
  • Position: nieuwe regels krijgen de eerstvolgende position na de bestaande regels
    (zelfde patroon als voegFactuurregelToe).
  • Server is bron van waarheid: de afleiding en validatie gebeuren server-side in de
    action; de UI-knop triggert alleen. Geen localStorage voor kernlogica (CLAUDE.md).
  • Geen nieuw scherm/navigatie: de actie leeft op de bestaande bewerk-pagina
    (/stal/facturen/[id]/bewerken); er komt geen nieuw scherm of sidebar-item bij (dat is
    Fact 07). Geen duplicatie met bestaande functionaliteit.

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:

  • Btw-tarief van extra diensten. Het contract legt per extra dienst geen apart
    btw-percentage vast. Voorstel: het tarief van de pensionprijs overnemen (meest gangbaar:
    zelfde tarief als de hoofddienst), en de regel blijft bewerkbaar zodat de stal een
    afwijkend tarief kan kiezen. Niet blokkerend.
  • Frequentie van extra diensten. De facturatie is in deze epic-fase maandelijks/
    handmatig (geen periodieke runs). Voorstel: alle extra-dienst-posten worden
    voorgevuld met quantity = 1 en de frequentie vermeld in de description
    (bv. "(per maand)" / "(eenmalig)" / "(per jaar)"), zodat de stal zelf bepaalt of een
    niet-maandelijkse post op deze factuur thuishoort en hem desgewenst verwijdert. Dit
    voorkomt dat een jaarpost stilzwijgend wordt weggelaten of dubbel gefactureerd. Niet
    blokkerend.
  • Afwijkend btw-percentage in het contract. VatRate kent enkel 0/9/21%. Voorstel:
    een afwijkend percentage mappen op het dichtstbijzijnde toegestane tarief; de regel
    blijft bewerkbaar. (In de praktijk staat het contract-btwPercentage op 21 of 9.) Niet
    blokkerend.
  • Dubbel voorvullen voorkomen. Voorstel: voorvullen alleen aanbieden/uitvoeren op een
    "verse" concept-factuur (regels = enkel de bij aanmaak verplichte regel, of nog niet
    eerder voorgevuld); bij herhaling een nette melding. De bouwer kiest de eenvoudigste
    sluitende variant. Niet blokkerend.

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