4.0 KiB
4.0 KiB
F-138 — Especificación técnica
Contrato de separación (F-154 precedent)
pricinges dueño depricing_variant_prices(tabla a su módulo). No se inserta desdecatalogcon SQL crudo — se expone un método en el servicio públicoPricingService(patrón idéntico al quecartya inyecta en build-app: cross-module write vía servicio público, R1 legal).
Cambios
1. pricing/domain/ports.ts
- Añadir a
PricingRepository:seedVariantPrice(variantId: string): Promise<void>. - Añadir a
PricingService:seedVariantPrice(variantId: string): Promise<void>.
2. pricing/application/pricing-service.ts
PricingService.seedVariantPrice(variantId)→ delega athis.repository.seedVariantPrice(variantId). Sin validación extra (elvariantIdya proviene de una variante creada en la misma transacción lógica).
3. pricing/infrastructure/pg-pricing-repository.ts
async seedVariantPrice(variantId: string): Promise<void> {
await this.pool.query(
`INSERT INTO pricing_variant_prices (variant_id, net_unit_amount_cents, offer_cents, cost_cents, vat_rate)
VALUES ($1, 0, NULL, NULL, 'general')
ON CONFLICT (variant_id) DO NOTHING`,
[variantId],
);
}
- Columnas idénticas al INSERT de
setVariantPrice(omitecurrency→ DDL default'EUR';created_at/updated_at→now()DDL default). ReutilizaVatRate = 'general'. ON CONFLICT (variant_id)válido:variant_ides UNIQUE/PK (usado porsetVariantPrice). → idempotente / re-sembbrado no-op.
4. catalog/application/variant-use-cases.ts
- Import (type, público, R1):
import type { PricingService } from '../../pricing/index.js';(catalog/application → ../../pricing/index = modules/pricing/index ✓ R1 a index público). CreateProductVariantrecibepricing: PricingServiceen ctor.execute: trasthis.variants.create(productId, input)(y solo si el producto existe),await this.pricing.seedVariantPrice(variant.id)best-effort (try/catch silencioso: la variante ya persistió; un fallo del seed no revierte la creación).
5. catalog/api/catalog.routes.ts
CatalogRoutesDeps+=pricing: PricingService(import type desde pricing index — R1).const createVariant = new CreateProductVariant(repository, variants, pricing);(único constructor; cubre los 3 call sites: POST /products autovariante, GET /products/:id/variants lazy, POST /products/:id/variants).
6. build-app.ts
- Mover
const pricing = createPricingService(deps.pool);antes del bloqueregisterCatalogRoutes(actualmente está después → orden L247 catalog, L264 pricing). Crear pricing antes del registro de catalog permite pasarlo aCatalogRoutesDeps. - Pasar
pricingen deps deregisterCatalogRoutes. - La ruta de pricing (
registerPricingRoutes) y cart siguen usandopricing(sin cambios; pricing sigue definido).createPricingServiceya está importado (L33).
Riesgos / mitigaciones
- Orden en build-app (timing): mover
const pricingantes de catalog es seguro (constructor puro,deps.pooldisponible). Precio routes lo vuelve a usar → no se rompe. - Fallo del seed: best-effort (try/catch) → no hace 500 en variant creation. AC3 lo prueba.
- Doble creación (createVariant en autovariante + retry):
ON CONFLICT DO NOTHING→ idempotente. AC2 lo prueba. - Boundary R1: catalog→pricing público index ✓ (cart ya lo usa). Deep imports prohibidos — usar
../../pricing/index.js.
Tests
- Unitario (runnable, sin DB):
catalog/tests/variant-use-cases.test.ts— FakeProductRepository, FakeProductVariantRepository, FakePricingService; assert (a) seedVariantPrice llamado con variant.id tras create, (b) no se sembran si producto no existe, (c) create sigue devolviendo variante si seed lanza. - Integración (AC): itest en
catalog.itest.tsskipIf(!hasDb) — POST /products/:id/variants → GET /pricing/variants/:id = 200 connetUnitAmountCents=0,vatRate='general'. Skipped sinTEST_DATABASE_URL(no bloquea verify.sh).