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). Fundament-eerst:
deze story levert de autorisatie- en inzage-laag (queries + guards) waarop Fact 03
(concept opstellen), Fact 05 (PDF/inzage) en Fact 07 (overzicht) voortbouwen.
User Story
Als gebruiker van Velaro (staleigenaar, stalmedewerker of paardeigenaar/leaser)
wil ik dat facturen server-side strikt zijn afgeschermd -- beheer alleen voor de
eigen stal, inzage alleen voor de eigen, verzonden facturen
zodat niemand ooit financiele gegevens van een andere stal, eigenaar of paard kan
opvragen, en elke partij precies ziet wat bij hem hoort.
Context
Fact 01 (#146) legde het datamodel vast: Invoice (met stableId naar uitgevende stal,
recipientUserId naar ontvanger, optionele contractId naar bron) en InvoiceLine. Er is
nog GEEN autorisatie- of inzage-laag. Conform CLAUDE.md is autorisatie kernlogica die
wij in de app-laag bouwen (niet uitbesteden aan Supabase); deze story implementeert dat
voor facturen, in lijn met het bestaande patroon van de contract-module.
De rollen uit CLAUDE.md:
OWNER / STAFF (stalrol via StableMember) -- beheren facturen van de eigen stal.
Paardeigenaar / leaser (geen stalrol) -- zien uitsluitend hun eigen facturen,
read-only, in de eigenaar-weergave (/eigenaar).
Platform-admin -- valt buiten deze story (geen factuurbeheer op platform-niveau).
Bestaand, te spiegelen patroon:
Stal-overzicht filtert server-side op stableId (getContractsForStable in
src/features/contracten/queries.ts).
Eigenaar-weergave filtert server-side op de gekoppelde gebruiker en sluit concepten
uit (getContractsForEigenaar: counterpartyUserId = userId + status != CONCEPT).
Beheeracties controleren de stalrol via getStableRole (assertCanManageContract-stijl
in src/features/contracten/actions.ts).
PDF/bijlage-inzage loopt via een signed URL, waarbij de aanroeper eerst de autorisatie
afdwingt en de helper enkel de URL genereert (getBijlagenMetUrls).
Scope
Binnen scope -- een nieuwe feature-map src/features/facturen/ met queries + guards:
Beheer-scoping (OWNER/STAFF):
Query die de facturen van een specifieke stal ophaalt, gefilterd op stableId,
inclusief recipient en contract voor de overzichtsregels. Nieuwste eerst.
Bij de schildwacht "alle stallen": beperken tot de stallen waar de gebruiker lid van
is (memberships), nooit een ongefilterde lijst.
Een autorisatie-helper (bijv. assertCanManageInvoice(userId, invoiceId) en/of
assertCanManageInvoicesForStable(userId, stableId)) die via getStableRole controleert
dat de gebruiker OWNER of STAFF is van de stal van de factuur; anders een fout
("Geen toegang"). Herbruikbaar door de latere beheeracties (Fact 03/07).
Een query om een factuur (met regels) voor beheer op te halen die de stal-scoping
afdwingt (factuur niet van de eigen stal -> niet teruggegeven / fout).
Inzage-scoping (eigenaar/leaser):
Query getInvoicesForRecipient(userId) die uitsluitend filtert op recipientUserId =
userId EN status != CONCEPT (concepten mogen de ontvanger nooit bereiken). Inclusief
de regels en de stal (afzender) voor weergave. Nieuwste eerst.
Een query om een factuur read-only voor de ontvanger op te halen die dezelfde twee
filters afdwingt (andere ontvanger of nog CONCEPT -> niet teruggegeven / fout).
Factuur-PDF-inzage via signed URL:
Een helper die een signed URL voor een factuur-PDF teruggeeft, met dezelfde
leesrechten als de eigenaar-weergave: de aanroeper dwingt eerst de autorisatie af
(eigenaar: eigen + verzonden; stalrol: eigen stal), de helper genereert enkel de
signed URL -- exact het patroon van getBijlagenMetUrls.
De helper neemt het opgeslagen storagePath als invoer en is robuust wanneer er (nog)
geen PDF is (Fact 05 genereert die pas) -> geeft null terug.
Een centrale autorisatiebeslissing per pad, server-side, zodat alle latere Fact-stories
deze helpers hergebruiken in plaats van de regels te dupliceren.
Buiten scope (latere Fact-stories):
UI/pagina's, knoppen of links naar facturen (overzicht = Fact 07, concept opstellen =
Fact 03). Deze story levert alleen de query/guard-laag.
Het genereren van de factuur-PDF + bucket-provisioning + opvolgende nummering
(Fact 05). Deze story regelt alleen de INZAGE (signed URL) op een bestaand storagePath.
Wijzigingen aan het Prisma-schema. Een opslagmodel voor de PDF hoort bij Fact 05;
deze story voegt geen kolommen/modellen toe.
Platform-admin factuurbeheer.
Acceptatiecriteria
Als een OWNER of STAFF de facturen van zijn eigen stal opvraagt, dan
krijgt hij uitsluitend facturen met die stableId terug (alle statussen, incl.
eigen concepten).
Als een gebruiker een factuur opvraagt van een stal waar hij geen OWNER/STAFF
van is, dan wordt deze geweigerd (fout / geen resultaat) -- server-side
afgedwongen, niet alleen in de UI.
Als een paardeigenaar/leaser zijn facturen opvraagt, dan krijgt hij
uitsluitend facturen terug waarvan recipientUserId zijn eigen id is EN die niet de
status CONCEPT hebben.
Als een eigenaar/leaser een factuur van een andere ontvanger, of een eigen
factuur die nog CONCEPT is, opvraagt, dan wordt deze geweigerd (geen resultaat
/ fout).
Wanneer inzage in een factuur-PDF wordt gevraagd, dan wordt eerst de
autorisatie afgedwongen en daarna een signed URL met beperkte TTL gegenereerd
(zelfde patroon als de contract-PDF/bijlagen); is er geen PDF, dan null.
De beheer-autorisatie accepteert zowel OWNER als STAFF; een gebruiker zonder
stalrol op die stal wordt geweigerd.
Alle autorisatieregels staan in src/features/facturen/ (queries + guard-helpers)
en zijn herbruikbaar door latere Fact-stories; er is geen UI/pagina toegevoegd.
npx tsc --noEmit slaagt (geen nieuwe type-errors); er zijn geen schema-wijzigingen.
Technische notities
Nieuwe map src/features/facturen/ met minimaal queries.ts en een guard-helper (bijv.
authorization.ts, of guards in queries.ts/actions.ts zoals de contract-module doet).
Hergebruik src/lib/auth/authorization.ts:
getStableRole(userId, stableId) -> OWNER/STAFF-check voor beheer.
getMemberships(userId) -> beperken tot eigen stallen bij de "alle stallen"-modus
(ALLE_STALLEN uit src/lib/active-stable.ts).
Spiegel de contract-queries als blauwdruk (src/features/contracten/queries.ts):
getContractsForStable (stal-scoping), getContractsForEigenaar (recipient-filter +
status != CONCEPT).
Authenticatie via getAuthUser() (src/lib/auth/session.ts); de laag krijgt de userId mee.
Factuur-PDF signed URL: volg getBijlagenMetUrls / bijlagenStorage.ts in
src/features/contracten/ -- admin-client (src/lib/supabase/admin.ts),
createSignedUrl(path, ttl), korte TTL (zoals de 600s bij contract-bijlagen). Bucket en
opslagpad worden door Fact 05 ingevuld; deze story neemt het pad als parameter en gokt
geen bucketnaam.
Geen localStorage voor autorisatie; alle beslissingen server-side (CLAUDE.md).
De eigenaar-weergave (src/app/(app)/eigenaar/page.tsx) is waar Fact 07 de eigenaar-
facturen toont; deze story levert de query, voegt zelf nog geen UI toe.
Open vragen
Geen blokkerende open vragen.
Niet-blokkerend (afgehandeld): de leaser-inzage volgt dezelfde regel als de eigenaar
(recipientUserId = userId + niet-CONCEPT). Of een actieve Lease als extra inzagepad
nodig is, hangt af van hoe Fact 03/07 de ontvanger zetten; zolang de factuur de leaser
als recipientUserId krijgt, is geen aparte lease-check nodig. Mocht later blijken dat
facturen op het paard i.p.v. de user ontsloten worden, dan is dat een aanpassing in
Fact 07, niet in dit fundament.
User Story
Als gebruiker van Velaro (staleigenaar, stalmedewerker of paardeigenaar/leaser)
wil ik dat facturen server-side strikt zijn afgeschermd -- beheer alleen voor de
eigen stal, inzage alleen voor de eigen, verzonden facturen
zodat niemand ooit financiele gegevens van een andere stal, eigenaar of paard kan
opvragen, en elke partij precies ziet wat bij hem hoort.
Context
Fact 01 (#146) legde het datamodel vast: Invoice (met stableId naar uitgevende stal,
recipientUserId naar ontvanger, optionele contractId naar bron) en InvoiceLine. Er is
nog GEEN autorisatie- of inzage-laag. Conform CLAUDE.md is autorisatie kernlogica die
wij in de app-laag bouwen (niet uitbesteden aan Supabase); deze story implementeert dat
voor facturen, in lijn met het bestaande patroon van de contract-module.
De rollen uit CLAUDE.md:
read-only, in de eigenaar-weergave (/eigenaar).
Bestaand, te spiegelen patroon:
src/features/contracten/queries.ts).
uit (getContractsForEigenaar: counterpartyUserId = userId + status != CONCEPT).
in src/features/contracten/actions.ts).
afdwingt en de helper enkel de URL genereert (getBijlagenMetUrls).
Scope
Binnen scope -- een nieuwe feature-map src/features/facturen/ met queries + guards:
Beheer-scoping (OWNER/STAFF):
inclusief recipient en contract voor de overzichtsregels. Nieuwste eerst.
is (memberships), nooit een ongefilterde lijst.
assertCanManageInvoicesForStable(userId, stableId)) die via getStableRole controleert
dat de gebruiker OWNER of STAFF is van de stal van de factuur; anders een fout
("Geen toegang"). Herbruikbaar door de latere beheeracties (Fact 03/07).
afdwingt (factuur niet van de eigen stal -> niet teruggegeven / fout).
Inzage-scoping (eigenaar/leaser):
userId EN status != CONCEPT (concepten mogen de ontvanger nooit bereiken). Inclusief
de regels en de stal (afzender) voor weergave. Nieuwste eerst.
filters afdwingt (andere ontvanger of nog CONCEPT -> niet teruggegeven / fout).
Factuur-PDF-inzage via signed URL:
leesrechten als de eigenaar-weergave: de aanroeper dwingt eerst de autorisatie af
(eigenaar: eigen + verzonden; stalrol: eigen stal), de helper genereert enkel de
signed URL -- exact het patroon van getBijlagenMetUrls.
geen PDF is (Fact 05 genereert die pas) -> geeft null terug.
Een centrale autorisatiebeslissing per pad, server-side, zodat alle latere Fact-stories
deze helpers hergebruiken in plaats van de regels te dupliceren.
Buiten scope (latere Fact-stories):
Fact 03). Deze story levert alleen de query/guard-laag.
(Fact 05). Deze story regelt alleen de INZAGE (signed URL) op een bestaand storagePath.
betaalwijze/SEPA (Fact 06).
deze story voegt geen kolommen/modellen toe.
Acceptatiecriteria
krijgt hij uitsluitend facturen met die stableId terug (alle statussen, incl.
eigen concepten).
van is, dan wordt deze geweigerd (fout / geen resultaat) -- server-side
afgedwongen, niet alleen in de UI.
uitsluitend facturen terug waarvan recipientUserId zijn eigen id is EN die niet de
status CONCEPT hebben.
factuur die nog CONCEPT is, opvraagt, dan wordt deze geweigerd (geen resultaat
/ fout).
autorisatie afgedwongen en daarna een signed URL met beperkte TTL gegenereerd
(zelfde patroon als de contract-PDF/bijlagen); is er geen PDF, dan null.
stalrol op die stal wordt geweigerd.
en zijn herbruikbaar door latere Fact-stories; er is geen UI/pagina toegevoegd.
Technische notities
authorization.ts, of guards in queries.ts/actions.ts zoals de contract-module doet).
(ALLE_STALLEN uit src/lib/active-stable.ts).
getContractsForStable (stal-scoping), getContractsForEigenaar (recipient-filter +
status != CONCEPT).
src/features/contracten/ -- admin-client (src/lib/supabase/admin.ts),
createSignedUrl(path, ttl), korte TTL (zoals de 600s bij contract-bijlagen). Bucket en
opslagpad worden door Fact 05 ingevuld; deze story neemt het pad als parameter en gokt
geen bucketnaam.
facturen toont; deze story levert de query, voegt zelf nog geen UI toe.
Open vragen
Geen blokkerende open vragen.
(recipientUserId = userId + niet-CONCEPT). Of een actieve Lease als extra inzagepad
nodig is, hangt af van hoe Fact 03/07 de ontvanger zetten; zolang de factuur de leaser
als recipientUserId krijgt, is geen aparte lease-check nodig. Mocht later blijken dat
facturen op het paard i.p.v. de user ontsloten worden, dan is dat een aanpassing in
Fact 07, niet in dit fundament.