75 lines
3.9 KiB
Markdown
75 lines
3.9 KiB
Markdown
# 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.
|