4.3 KiB
4.3 KiB
F-143 — Implementer evidence
What
Establishes the shared reporting filter contract + RBAC foundation (reporting module, backend-only, no DB reads, no migration). New routes GET /reporting/filters/schema (REPORTING_VIEW) and GET /reporting/filters/validate (REPORTING_SALES); zod reportingFiltersSchema ([from,to) semantics), comparisonRange helper, and a role→permission map (REPORTING_ROLE_PERMISSIONS) + requireReportingPermission. Wired in build-app.ts inside if (deps.pool) with combinedAuth.
Design recap (architect-approved — see architect.md)
reporting/layered api→application→domain (mirrors pricing): pure typesdomain/filters.ts(const arraysREPORTING_COMPARISON/CHANNELS/GROUP_BYderive their union types DRY); zod schema + parser +comparisonRange+REPORTING_FILTER_METAinapplication/filters.ts;registerReportingRoutesinapi/reporting.routes.ts; public surface inindex.ts.- From/to are
z.string().refine(!isNaN(Date.parse))(zod-v4-safe;.datetime()API moved in zod v4). Range semantics[from,to)viarefine(from < to)→ 400 on inversion. Repeatable UUID fields usez.preprocess(single string ↔ array) because Fastify querystring yields string vs array. - RBAC: no permission table exists → role→permission map +
requireReportingPermission(user, perm)(future table migration keeps call-site signature). FINANCIAL/EXPORT admin-only; editor/pos_* least-privilege. dataAvailabilityis metadata only (F-142 §4 baseline); never converts unavailable→0; no report data computed (F-144+).- Routes have NO 200 response schema (broad passthrough, matching security.routes.ts precedent) so the dynamic DTO isn't stripped by fast-json-stringify; error responses use the shared
errorSchema.
Files created
project/src/modules/reporting/domain/filters.ts— const arrays + pure types (ReportingFilters,ReportingFilterMeta,DateRange,ComparisonRange,DataAvailability, etc.).project/src/modules/reporting/domain/permissions.ts—ReportingPermission,REPORTING_ROLE_PERMISSIONS: Record<Role, ReportingPermission[]>,requireReportingPermission,userReportingPermissions.project/src/modules/reporting/application/filters.ts—reportingFiltersSchema,parseReportingFilters,comparisonRange,REPORTING_FILTER_META,REPORTING_PAGE_SIZE_MAX.project/src/modules/reporting/api/reporting.routes.ts—registerReportingRoutes+ReportingRoutesDeps { authenticate }.project/src/modules/reporting/index.ts— public re-exports.project/src/modules/reporting/tests/filters.test.ts,tests/permissions.test.ts,api/reporting.routes.test.ts.- EDIT
project/src/app/build-app.ts—import { registerReportingRoutes } from '../modules/reporting/index.js'+ register block insideif (deps.pool)usingcombinedAuth.
Tests
- NEW
reporting/tests/filters.test.ts(9): defaults, repeatable uuid (single+array), from>=to→throw, invalid datetime, pageSize ceiling, invalid uuid→throw, comparisonRange (none/previous_equal/previous_calendar). - NEW
reporting/tests/permissions.test.ts(6): admin all-grants, pos_cashier least-privilege, customer none, requireReportingPermission 403 for denied, FINANCIAL admin-only (AC6), matrix covers every role. - NEW
reporting/api/reporting.routes.test.ts(7): schema route admin 200 + contract; customer 403 (×2); validate 200 + comparison invariants; defaults; inverted→400; missing→400; customer 403. Mirrorssecurity.routes.test.ts(minimal Fastify + mock authenticate, no DB) and installs the build-app errorHandler/serializer so AppError(400/403) surface as real status codes.
Verification
npx tsc --noEmit→ 0 errors (strict, noUncheckedIndexedAccess).npx vitest run(full suite) → 229 passed | 57 skipped (DB itests skipped w/oTEST_DATABASE_URL); reporting contributes +22 (15 unit + 7 route); 0 regressions vs F-138 baseline (209 passed).node project/scripts/check-module-boundaries.mjs project/src→ 0 NEW violations forreporting/(imports onlyshared/*+zod). The sole remaining violation (security/routes.ts → log-broadcaster, an R1 deep-import) is pre-existing (introduced by F-154), untouched by F-143 — confirmed out of scope../scripts/verify.sh→ green (F-143in_progressis runtime-consistent; nopending/donemismatch).