fix(timetable): align Zod decimal fields with backend wire format (decimal-as-string per Laravel cast) (B5)
Phase A diagnosed the "Kon timetable niet laden" browser symptom as Zod
schema drift. The prompt's hypothesis (enum {value, label} mismatch) was
incorrect — the schema already uses the enumLabel() wrapper for every
enum field. The actual drift is decimal-cast columns: Laravel serialises
`decimal(N,M)` columns as strings to preserve precision, but the schema
expected numbers, so the very first response triggered a ZodError.
Affected fields, all on `artist_engagements`:
fee_amount decimal(10,2) → wire `"11503.58"`, schema was z.number()
buma_percentage decimal(5,2) → wire `"7.00"`, schema was z.number()
vat_percentage decimal(5,2) → wire `"21.00"`, schema was z.number()
deposit_percentage decimal(5,2) → wire `"…"`, schema was z.number()
Backend has no explicit `decimal:N` cast on these columns
(api/app/Models/ArtistEngagement.php:64-85 — the `casts()` method covers
the enums + booleans + dates + integers, but skips decimals).
Per the strategic decision (frontend adapts, backend stays):
- schemas/timetable.ts: four fields → z.string().nullable()
- types/timetable.ts: matching ArtistEngagement interface fields →
`string | null`
- PerformancePopover.vue:129: only consumer doing arithmetic on a
decimal field; coerce at the use site via Number(...).toFixed(2).
Single line.
- tests/component/PerformanceBlock.test.ts + tests/a11y/axe.test.ts:
spot-checked mocks; the two with hand-built engagement payloads
flipped fee_amount/buma_percentage/vat_percentage from numbers to
strings to match the new schema. No other mocks needed updating.
The {value, label} enum wrapper claim in the prompt was specifically
debunked in Phase A — every consumer (Wachtrij, PerformanceBlock,
WachtrijCard, PerformancePopover, AddPerformanceDialog, page entry)
already uses .value/.label access against an enumLabel-wrapped schema.
B6 will lock the wire-format contract with a real-API fixture
regression test.
All 397 tests still pass; typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ const engagement: Partial<ArtistEngagement> = {
|
||||
artist: { id: 'a1', name: 'Devin Wild' } as ArtistEngagement['artist'],
|
||||
booking_status: { value: ArtistEngagementStatus.CONFIRMED, label: 'Bevestigd' },
|
||||
computed: { buma_amount: 70, vat_grondslag: 1070, vat_amount: 224.7, breakdown_total: 0, total_cost: 1294.7 },
|
||||
fee_amount: 1000,
|
||||
fee_amount: '1000.00',
|
||||
advancing_completed_count: 3,
|
||||
advancing_total_count: 5,
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ function makePerformance(overrides: Partial<Performance> = {}): Performance {
|
||||
event_id: 'ev1',
|
||||
booking_status: { value: ArtistEngagementStatus.CONFIRMED, label: 'Bevestigd' },
|
||||
project_leader_id: null,
|
||||
fee_amount: 1000,
|
||||
fee_amount: '1000.00',
|
||||
fee_currency: 'EUR',
|
||||
fee_type: { value: null, label: null },
|
||||
buma_applicable: true,
|
||||
buma_percentage: 7,
|
||||
buma_percentage: '7.00',
|
||||
buma_handled_by: { value: null, label: null },
|
||||
vat_applicable: true,
|
||||
vat_percentage: 21,
|
||||
vat_percentage: '21.00',
|
||||
deal_breakdown: null,
|
||||
deposit_percentage: null,
|
||||
deposit_due_date: null,
|
||||
|
||||
Reference in New Issue
Block a user