feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,74 @@
# Architect — F-017 Pricing module
## Feature
F-017 adds server-side pricing and VAT calculation behind a public `PricingService`, so future cart/checkout flows never trust frontend-supplied prices.
## Design
### Module boundaries
Create `project/src/modules/pricing/` following existing module structure:
- `domain/price.ts` for money, VAT rate, price record and calculation result types.
- `domain/ports.ts` for `PricingRepository` and `PricingService` contracts.
- `domain/errors.ts` for missing price and invalid pricing command errors.
- `application/pricing-service.ts` implementing calculations and price changes.
- `infrastructure/pg-pricing-repository.ts` owning PostgreSQL persistence.
- `api/pricing.routes.ts` for explicit backend endpoints.
- `index.ts` exporting only the public pricing API.
Catalog remains independent. Pricing stores `variant_id` as an opaque UUID reference and does not join or import catalog internals. Future cart/checkout must depend on `PricingService`, injected at composition root.
### Money and VAT rules
Use integer cents only — no floating point money.
- Stored base price: `net_unit_amount_cents` (pre-VAT).
- VAT rate: enum `general` = 21% and `reduced` = 10% for this slice.
- Calculation: `netSubtotal = netUnit * quantity`; `vatAmount = round(netSubtotal * vatBps / 10000)`; `total = netSubtotal + vatAmount`.
- Currency is fixed to `EUR` by database default and API response. Multi-currency is explicitly out of scope.
### Data model
Add migration `012_pricing.js`:
- `pricing_variant_prices`
- `variant_id uuid primary key`
- `net_unit_amount_cents integer not null check >= 0`
- `vat_rate text not null check in ('general', 'reduced')`
- `currency text not null default 'EUR' check = 'EUR'`
- timestamps
- `pricing_price_history`
- append-only audit rows for every price change
- stores previous and new net amount/rate/currency, plus timestamp
- first price creation also writes a history row with previous fields null
### Public service interface
Expose `PricingService` from module index:
- `calculate({ variantId, quantity }): Promise<PriceCalculation>`
- `setVariantPrice({ variantId, netUnitAmountCents, vatRate }): Promise<VariantPrice>`
- `getVariantPrice(variantId): Promise<VariantPrice | undefined>`
`calculate` never accepts or uses client unit price, subtotal, VAT, discount, or total fields. That is the core rule: callers provide product identity and quantity; pricing returns truth.
### API slice
Add routes:
- `GET /pricing/variants/:variantId` — public read of current server-side price.
- `PUT /pricing/variants/:variantId` — admin-only price change.
- `POST /pricing/calculate` — calculation by `variantId` and `quantity`; schema strips/ignores extra client fields.
HTTP errors:
- `PRICING_PRICE_NOT_FOUND` as 404 when calculating/read has no server-side price.
- `INVALID_PRICE` as 422 for invalid commands.
### Tests
- Unit tests for VAT calculation for general/reduced rates and invalid quantity/amount.
- Unit/API test proving client-supplied price fields are ignored by schema/service and server price is used.
- Integration test proving every price change writes a history row, including create and update.
- Boundary test proving catalog does not reference `pricing_` tables or pricing internals.
- Migration suite remains the safety net for up/down/idempotency.
## Acceptance trace
- PricingService.calculate returns totals with VAT from persisted server price.
- Client price ignored: calculate only uses variant id + quantity; schema strips extra price fields and tests send bogus client values.
- Price history: repository writes append-only rows on creation and update.
- `verify.sh`: must pass after gates.
## Risks / constraints
- Do not add decimal/money dependencies; integer cents are sufficient and safer.
- Do not couple pricing to catalog by FK or join in this slice; variant existence validation is a later orchestration concern.
- Do not store or trust client totals anywhere.

View File

@@ -0,0 +1,39 @@
# Documenter — F-017 Pricing module
## Summary
Documented F-017 pricing behavior in this evidence artifact. Project README updates are not applied during document stage because the active repository guard restricts `project/` product edits to build/implementer state.
## Public API notes
Pricing owns server-side price and VAT truth for product variants. Callers provide variant identity and quantity only; client-supplied prices, tax amounts, discounts, subtotals, or totals are not trusted.
| Route | Access | Result |
| --------------------------------- | ---------- | ------------------------------------------- |
| GET /pricing/variants/:variantId | public | `200` current server-side variant price |
| PUT /pricing/variants/:variantId | admin only | `200` updated price and history row written |
| POST /pricing/calculate | public | `200` calculated net subtotal, VAT and total |
## Error codes
- `PRICING_PRICE_NOT_FOUND` — HTTP 404
- `INVALID_PRICE` — HTTP 422
## Money and VAT rules
- Money is represented as integer cents only.
- Currency is fixed to `EUR`.
- Stored price is `netUnitAmountCents` before VAT.
- VAT rates:
- `general` = 21% (`2100` basis points)
- `reduced` = 10% (`1000` basis points)
- Calculation: `netSubtotal = netUnitAmountCents * quantity`; `vatAmount = round(netSubtotal * vatBps / 10000)`; `total = netSubtotal + vatAmount`.
## Audit/history
- Current price lives in `pricing_variant_prices`.
- Every create/update writes an append-only row to `pricing_price_history` with previous and new values.
- Variant ids are opaque UUIDs; pricing does not import catalog internals or join catalog tables.
## Evidence
- `work/artifacts/F-017/architect.md`
- `work/artifacts/F-017/implementer.md`
- `work/artifacts/F-017/reviewer.json`
- `work/artifacts/F-017/security.json`
- `work/artifacts/F-017/qa.json`

View File

@@ -0,0 +1,38 @@
# Implementer — F-017 Pricing module
## Summary
Implemented server-side pricing with VAT behind a public `PricingService`, including PostgreSQL persistence, append-only price history, API routes, migration, and tests.
## Files changed
- `project/migrations/012_pricing.js`
- `project/src/app/build-app.ts`
- `project/src/app/tests/pricing.itest.ts`
- `project/src/modules/pricing/index.ts`
- `project/src/modules/pricing/api/pricing.routes.ts`
- `project/src/modules/pricing/application/pricing-service.ts`
- `project/src/modules/pricing/domain/errors.ts`
- `project/src/modules/pricing/domain/ports.ts`
- `project/src/modules/pricing/domain/price.ts`
- `project/src/modules/pricing/infrastructure/pg-pricing-repository.ts`
- `project/src/modules/pricing/tests/boundary.test.ts`
- `project/src/modules/pricing/tests/pricing-service.test.ts`
## Acceptance evidence
- AC1 variant + quantity returns total with VAT: covered by `pricing-service.test.ts` and `pricing.itest.ts`.
- AC2 client-supplied price ignored: `pricing.itest.ts` sends bogus client price fields to `/pricing/calculate`; response uses persisted server-side price.
- AC3 price change writes history row: `pricing.itest.ts` verifies creation and update both append rows to `pricing_price_history`.
- `verify.sh`: passed.
## Commands run
- `cd project && npm run typecheck` — passed
- `cd project && npm run lint` — passed after Prettier formatting
- `cd project && npm run build` — passed
- `cd project && npm test` — passed; DB integration tests skipped without `TEST_DATABASE_URL`
- `cd project && TEST_DATABASE_URL='postgres://mdv:mdv_dev_only@localhost:5432/mdv_test' npm run test:integration -- pricing.itest migrations.itest` — passed; 9 files, 39 tests
- `./scripts/verify.sh` — passed
## Notes
- No new runtime dependency was added.
- Money is represented as integer cents only.
- VAT rates are `general` 21% and `reduced` 10%.
- Pricing stores `variant_id` as an opaque UUID and does not import or join catalog internals.

View File

@@ -0,0 +1,15 @@
{
"feature_id": "F-017",
"agent": "leader",
"verdict": "APPROVED",
"summary": "F-017 closed with reviewer, security and QA gates approved. Final verify.sh passed.",
"evidence": [
"reviewer.json verdict APPROVED",
"security.json verdict APPROVED",
"qa.json verdict APPROVED",
"./scripts/verify.sh passed during close",
"backlog/features.json updated: F-017 status done and gates true",
"work/current.md updated: no active feature, next suggested F-018"
],
"timestamp": "2026-08-15T16:25:43Z"
}

View File

@@ -0,0 +1,48 @@
{
"feature_id": "F-017",
"agent": "qa",
"verdict": "APPROVED",
"summary": "QA approved. All F-017 acceptance criteria are traced to implementation and executable evidence. The PostgreSQL integration suite passed with TEST_DATABASE_URL, including server-side VAT calculation, ignored client price fields, and append-only price history.",
"evidence": [
"PricingService.calculate AC covered by project/src/modules/pricing/tests/pricing-service.test.ts and project/src/app/tests/pricing.itest.ts",
"Client-supplied price ignored AC covered by pricing.itest.ts payload with bogus netUnitAmountCents/totalCents/vatAmountCents",
"Price history AC covered by pricing.itest.ts querying pricing_price_history after create and update",
"cd project && TEST_DATABASE_URL='postgres://mdv:mdv_dev_only@localhost:5432/mdv_test' npm run test:integration -- pricing.itest migrations.itest passed: 9 integration files, 39 tests",
"cd project && npm run lint passed",
"cd project && npm run typecheck passed",
"cd project && npm run build passed",
"cd project && npm test passed: 21 passed, 9 skipped without TEST_DATABASE_URL",
"./scripts/verify.sh passed"
],
"acceptance": [
{
"criterion": "Given variant and quantity When PricingService.calculate Then total with VAT returned",
"status": "PASS",
"evidence": "Pricing unit/integration tests calculate 1000 cents x2 with 21% VAT as totalCents=2420"
},
{
"criterion": "Given client-supplied price in any request Then price ignored and recalculated",
"status": "PASS",
"evidence": "pricing.itest.ts sends bogus client price fields to /pricing/calculate and response uses persisted server price 500 cents with reduced VAT"
},
{
"criterion": "Price change writes history row, never silent update",
"status": "PASS",
"evidence": "pricing.itest.ts verifies pricing_price_history has one row for initial create and one row for update with previous/new values"
},
{
"criterion": "verify.sh green",
"status": "PASS",
"evidence": "./scripts/verify.sh PASS"
}
],
"commands": [
"cd project && npm run lint: PASS",
"cd project && npm run typecheck: PASS",
"cd project && npm run build: PASS",
"cd project && npm test: PASS; 21 passed, 9 skipped when TEST_DATABASE_URL absent",
"cd project && TEST_DATABASE_URL='postgres://mdv:mdv_dev_only@localhost:5432/mdv_test' npm run test:integration -- pricing.itest migrations.itest: PASS; 9 files, 39 tests",
"./scripts/verify.sh: PASS"
],
"timestamp": "2026-08-15T16:24:50Z"
}

View File

@@ -0,0 +1,23 @@
{
"feature_id": "F-017",
"agent": "reviewer",
"verdict": "APPROVED",
"summary": "F-017 review approved. The pricing module is isolated, exposes a public PricingService contract, calculates VAT using integer cents from persisted server-side prices, strips client-supplied price fields from calculate requests, and writes append-only history on every price change.",
"evidence": [
"Read work/current.md, architect.md and implementer.md",
"Inspected pricing domain/application/infrastructure/API files: public API exported from index.ts; internals stay inside modules/pricing",
"Inspected PricingService: calculation uses persisted VariantPrice plus quantity only; money is integer cents; VAT basis points are explicit",
"Inspected pricing routes: /pricing/calculate schema strips extra fields and never accepts client totals as truth",
"Inspected PgPricingRepository: setVariantPrice locks current row and writes pricing_price_history inside the same transaction",
"Inspected migration 012_pricing.js: current price table and append-only history table with VAT/currency/amount constraints",
"Inspected boundary test: catalog source scan blocks pricing table references and pricing internal imports",
"gentle-ai review mode status: receipt-driven development off globally, ordinary Orquestra gate used",
"cd project && npm run lint passed",
"cd project && npm run typecheck passed",
"cd project && npm run build passed",
"cd project && npm test passed; DB integration tests skipped without TEST_DATABASE_URL",
"cd project && TEST_DATABASE_URL='postgres://mdv:mdv_dev_only@localhost:5432/mdv_test' npm run test:integration -- pricing.itest migrations.itest passed",
"./scripts/verify.sh passed"
],
"timestamp": "2026-08-15T16:24:16Z"
}

View File

@@ -0,0 +1,17 @@
{
"feature_id": "F-017",
"agent": "security",
"verdict": "APPROVED",
"summary": "Security approved. No new dependencies or committed secrets were introduced. Price mutations require admin authentication, request input is validated with zod, SQL uses parameterized queries, and client-supplied price/tax totals are stripped and ignored by calculation.",
"evidence": [
"cd project && npm audit --audit-level=high --omit=dev: found 0 vulnerabilities",
"Secret scan over project/src/modules/pricing, project/migrations/012_pricing.js and project/src/app/tests/pricing.itest.ts found only a test fixture password, no committed credentials",
"Reviewed pricing routes: PUT /pricing/variants/:variantId requires authenticated admin role; calculate route accepts only variantId and quantity",
"Reviewed validation: variantId is z.uuid(); amounts and quantities are integer-bounded; VAT rate is enum general/reduced",
"Reviewed calculate schema: `.strip()` removes client-supplied totals/prices before service calculation",
"Reviewed repository SQL: all dynamic values are parameterized; no user input is interpolated into SQL text",
"Reviewed migration: CHECK constraints enforce non-negative amounts, allowed VAT rates, and EUR-only currency",
"No new runtime dependency added"
],
"timestamp": "2026-08-15T16:24:35Z"
}