# 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.
