Skip to content

[Fact 02] Autorisatie & inzage facturen #147

Description

@MiniMaxi-user

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.
  • Statusovergangen/verzenden/herinneringen (Fact 07), bedrag-/btw-berekening (Fact 04),
    betaalwijze/SEPA (Fact 06).
  • 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.

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