feat(ADM-018): completed feature
This commit is contained in:
74
work/artifacts/F-017/architect.md
Normal file
74
work/artifacts/F-017/architect.md
Normal 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.
|
||||
39
work/artifacts/F-017/documenter.md
Normal file
39
work/artifacts/F-017/documenter.md
Normal 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`
|
||||
38
work/artifacts/F-017/implementer.md
Normal file
38
work/artifacts/F-017/implementer.md
Normal 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.
|
||||
15
work/artifacts/F-017/leader-close.json
Normal file
15
work/artifacts/F-017/leader-close.json
Normal 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"
|
||||
}
|
||||
48
work/artifacts/F-017/qa.json
Normal file
48
work/artifacts/F-017/qa.json
Normal 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"
|
||||
}
|
||||
23
work/artifacts/F-017/reviewer.json
Normal file
23
work/artifacts/F-017/reviewer.json
Normal 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"
|
||||
}
|
||||
17
work/artifacts/F-017/security.json
Normal file
17
work/artifacts/F-017/security.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user