feat: PRC-001 WordCounter + ChargeableCharConfig (SPIKE + scope delta) #59

Merged
dmolinari merged 1 commits from feature/PRC-001 into main 2026-04-21 16:06:20 +00:00
Owner

Resumen

Implementa PRC-001 (WordCounter + extracción de caracteres especiales, SPIKE) más el scope delta que emergió post-smoke-test: el config de caracteres pasó de per-Medio a per-ProductType, con seed global a 0 (opt-in billing), endpoints de Reactivar (A+guard) y Eliminar, y UI condicional por isActive.

⚠️ Nota de proceso: esta PR se crea retroactivamente. El merge directo a main (commit 0e2e4c9) fue ejecutado por el agente de archive antes de crear la PR, en lugar del flujo PR-first habitual del repo (ver PR #45 PRD-003). Esta PR documenta retrospectivamente todo el scope + agrega 1 commit pendiente (database/README.md con V018..V024). Próximas UDTs vuelven al flujo PR-first estándar.

Scope entregado

Core SPIKE (commits 9144c2e..5175cc1 — 8 commits)

  • Dominio: WordCounterService puro con pipeline de 9 pasos (normalize + detect specials + anti-fraude + tokenize). WordCountResult value object. 4 excepciones de dominio.
  • 25 golden cases individualmente asertados (WordCounterGoldenCasesTests) — acceptance gate del SPIKE.
  • BD: dbo.ChargeableCharConfig + _History con SYSTEM_VERSIONING (retention 10y). SP InsertWithClose con forward-only semantics. Permiso RBAC tasacion:caracteres_especiales:gestionar.
  • Backend: commands/queries + handlers + validators + IAuditLogger fail-closed dentro de TransactionScope. Repository Dapper. Controller admin con 5 endpoints.
  • Frontend: feature chargeableChars con table + form dialog + emoji-blocker SymbolInput + copy-to-all quick action.
  • Tests hardening: concurrency (3-way barrier), SYSTEM_VERSIONING snapshot, per-X + global fallback, WordCounter × ChargeableChar integration contract.

Scope delta post-smoke (commits 5c1675e..3a59608 + ee36d86 — 6 commits)

Tras smoke test (2026-04-20), el usuario identificó 4 ajustes que se resolvieron pre-merge sin salir de la branch:

  1. Per-ProductType en lugar de Per-Medio (V023 + V024): MedioIdProductTypeId con FK a dbo.ProductType. SP renombrado a GetActiveForProductType. Seed global a PricePerUnit=0.0000 (opt-in billing).
  2. Reactivar (A+guard): endpoint PATCH /api/v1/admin/chargeable-chars/{id}/reactivate + SP ReactivateWithGuard con 4 guards (not found / already active / vigente exists / posterior rows) → 409 con reason.
  3. Eliminar: endpoint DELETE /api/v1/admin/chargeable-chars/{id}. Guard de uso DIFERIDO a FAC-001 (callout [!important] en doc UDT FAC-001 recuerda la obligación).
  4. UI condicional: isActive=true → Desactivar + Eliminar; isActive=false → Reactivar + Eliminar. Mensaje genérico del delete dialog (sin referencia FAC-001 hardcodeada).

Bugs detectados y arreglados en esta branch

  • 500 en Reactivate (commit d7c6cbd): dos SqlConnection en el mismo TransactionScope promovían la tx a DTC → MSDTC no habilitado → excepción opaca. Fix: SP + SELECT en la MISMA connection (local LTM).
  • V023 no idempotente (commit 40b5f39): SET SYSTEM_VERSIONING = OFF fallaba si ya estaba OFF. Fix: guard con temporal_type=2.
  • Schema mismatch en tests de ProductType (commit d7c6cbd): tests usaban (Nombre, Codigo, Activo) inexistentes. Fix: usar (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages) como los otros tests.
  • SqlTestFixture V021 ALTER con MedioId (commit d7c6cbd): ALTER fallaba en segunda corrida post-V023. Fix: guard IF hasMedioId.

Migraciones

V Archivo Descripción
V020 V020__add_chargeable_chars_permission.sql Permiso tasacion:caracteres_especiales:gestionar + asignación a admin
V021 V021__create_chargeable_char_config.sql Tabla + Temporal 10y + 2 SPs + 2 índices
V022 V022__seed_chargeable_char_config.sql Seed 4 globales ($, %, !, ¡)
V023 V023__refactor_chargeable_char_config_to_product_type.sql MedioId→ProductTypeId + SP ReactivateWithGuard + CK_Price_NonNegative
V024 V024__reseed_global_with_zero_price.sql Reseed a 0.0000 (opt-in billing)

Todas aplicadas en SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api.

Verify

  • verify-report-v2 (engram sdd/prc-001-word-counter-spike/verify-report-v2): PASS-WITH-FOLLOWUPS
  • Tests: 1634 .NET + 510 vitest todos green. 0 tsc errors.
  • 0 CRITICAL, 5 WARNING, 4 SUGGESTION — todas no bloqueantes, mapeadas a followups.

Followups creados (7 issues label followup)

  • #52 R1.7 hyphens default
  • #53 GC-24 all-specials=0 words
  • #54 V022/V024 precios reales pre-go-live
  • #55 R2.7 emoji en Symbol → PRC-009
  • #56 [FAC-001] Guards de uso en Delete + Reactivate (heredado crítico)
  • #57 FluentValidation alineación opt-in billing
  • #58 tsconfig deprecation + MSW handler

Documentación

  • Obsidian/STATUS.md: PRC-001 [✅], Critical Path apunta a PRC-002.
  • Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md: entidad agregada al catálogo.
  • Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.8 📋 UDTs Módulo Tasación.md: sección Implementación con hashes.
  • Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.12 📋 UDTs Módulo Facturación.md: callout [!important] en FAC-001 con guards obligatorios (imposible de ignorar al abrir FAC-001).
  • Obsidian/06-API-y-CONTRATOS/6.3 📊 API Tasación.md: doc creado desde cero con contratos.
  • database/README.md: V018..V024 agregadas (este commit final ee36d86).

Artifacts engram

  • sdd/prc-001-word-counter-spike/archive-report — status ARCHIVED
  • sdd/prc-001-word-counter-spike/verify-report-v2 — pass final
  • sdd/prc-001-word-counter-spike/scope-delta-1 — historia del refactor post-smoke
  • sdd/prc-001-word-counter-spike/apply-progress — journey completo

Closes #56 parcialmente (guards diferidos a FAC-001). No cierra #52..#55 ni #57/#58 (followups de PRC-001 standalone).

## Resumen Implementa **PRC-001** (WordCounter + extracción de caracteres especiales, SPIKE) más el **scope delta** que emergió post-smoke-test: el config de caracteres pasó de per-`Medio` a per-`ProductType`, con seed global a 0 (opt-in billing), endpoints de **Reactivar** (A+guard) y **Eliminar**, y UI condicional por `isActive`. > ⚠️ **Nota de proceso**: esta PR se crea **retroactivamente**. El merge directo a `main` (commit `0e2e4c9`) fue ejecutado por el agente de archive antes de crear la PR, en lugar del flujo PR-first habitual del repo (ver PR #45 PRD-003). Esta PR documenta retrospectivamente todo el scope + agrega 1 commit pendiente (`database/README.md` con V018..V024). Próximas UDTs vuelven al flujo PR-first estándar. ## Scope entregado ### Core SPIKE (commits 9144c2e..5175cc1 — 8 commits) - **Dominio**: `WordCounterService` puro con pipeline de 9 pasos (normalize + detect specials + anti-fraude + tokenize). `WordCountResult` value object. 4 excepciones de dominio. - **25 golden cases** individualmente asertados (`WordCounterGoldenCasesTests`) — acceptance gate del SPIKE. - **BD**: `dbo.ChargeableCharConfig` + `_History` con SYSTEM_VERSIONING (retention 10y). SP `InsertWithClose` con forward-only semantics. Permiso RBAC `tasacion:caracteres_especiales:gestionar`. - **Backend**: commands/queries + handlers + validators + `IAuditLogger` fail-closed dentro de TransactionScope. Repository Dapper. Controller admin con 5 endpoints. - **Frontend**: feature `chargeableChars` con table + form dialog + emoji-blocker `SymbolInput` + copy-to-all quick action. - **Tests hardening**: concurrency (3-way barrier), SYSTEM_VERSIONING snapshot, per-X + global fallback, WordCounter × ChargeableChar integration contract. ### Scope delta post-smoke (commits 5c1675e..3a59608 + ee36d86 — 6 commits) Tras smoke test (2026-04-20), el usuario identificó 4 ajustes que se resolvieron pre-merge sin salir de la branch: 1. **Per-ProductType en lugar de Per-Medio** (V023 + V024): `MedioId` → `ProductTypeId` con FK a `dbo.ProductType`. SP renombrado a `GetActiveForProductType`. Seed global a `PricePerUnit=0.0000` (opt-in billing). 2. **Reactivar (A+guard)**: endpoint `PATCH /api/v1/admin/chargeable-chars/{id}/reactivate` + SP `ReactivateWithGuard` con 4 guards (not found / already active / vigente exists / posterior rows) → 409 con `reason`. 3. **Eliminar**: endpoint `DELETE /api/v1/admin/chargeable-chars/{id}`. Guard de uso DIFERIDO a FAC-001 (callout `[!important]` en doc UDT FAC-001 recuerda la obligación). 4. **UI condicional**: `isActive=true` → Desactivar + Eliminar; `isActive=false` → Reactivar + Eliminar. Mensaje genérico del delete dialog (sin referencia FAC-001 hardcodeada). ### Bugs detectados y arreglados en esta branch - **500 en Reactivate** (commit `d7c6cbd`): dos `SqlConnection` en el mismo `TransactionScope` promovían la tx a DTC → MSDTC no habilitado → excepción opaca. Fix: SP + SELECT en la MISMA connection (local LTM). - **V023 no idempotente** (commit `40b5f39`): `SET SYSTEM_VERSIONING = OFF` fallaba si ya estaba OFF. Fix: guard con `temporal_type=2`. - **Schema mismatch en tests de ProductType** (commit `d7c6cbd`): tests usaban `(Nombre, Codigo, Activo)` inexistentes. Fix: usar `(Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages)` como los otros tests. - **SqlTestFixture V021 ALTER con MedioId** (commit `d7c6cbd`): ALTER fallaba en segunda corrida post-V023. Fix: guard `IF hasMedioId`. ## Migraciones | V | Archivo | Descripción | |---|---|---| | V020 | `V020__add_chargeable_chars_permission.sql` | Permiso `tasacion:caracteres_especiales:gestionar` + asignación a admin | | V021 | `V021__create_chargeable_char_config.sql` | Tabla + Temporal 10y + 2 SPs + 2 índices | | V022 | `V022__seed_chargeable_char_config.sql` | Seed 4 globales (`$`, `%`, `!`, `¡`) | | V023 | `V023__refactor_chargeable_char_config_to_product_type.sql` | MedioId→ProductTypeId + SP ReactivateWithGuard + CK_Price_NonNegative | | V024 | `V024__reseed_global_with_zero_price.sql` | Reseed a 0.0000 (opt-in billing) | Todas aplicadas en `SIGCM2`, `SIGCM2_Test_App`, `SIGCM2_Test_Api`. ## Verify - **verify-report-v2** (engram `sdd/prc-001-word-counter-spike/verify-report-v2`): **PASS-WITH-FOLLOWUPS** - Tests: **1634 .NET + 510 vitest todos green**. 0 tsc errors. - 0 CRITICAL, 5 WARNING, 4 SUGGESTION — todas no bloqueantes, mapeadas a followups. ## Followups creados (7 issues label `followup`) - #52 R1.7 hyphens default - #53 GC-24 all-specials=0 words - #54 V022/V024 precios reales pre-go-live - #55 R2.7 emoji en Symbol → PRC-009 - #56 **[FAC-001] Guards de uso en Delete + Reactivate** (heredado crítico) - #57 FluentValidation alineación opt-in billing - #58 tsconfig deprecation + MSW handler ## Documentación - `Obsidian/STATUS.md`: PRC-001 `[✅]`, Critical Path apunta a PRC-002. - `Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md`: entidad agregada al catálogo. - `Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.8 📋 UDTs Módulo Tasación.md`: sección ✅ Implementación con hashes. - `Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.12 📋 UDTs Módulo Facturación.md`: **callout `[!important]`** en FAC-001 con guards obligatorios (imposible de ignorar al abrir FAC-001). - `Obsidian/06-API-y-CONTRATOS/6.3 📊 API Tasación.md`: doc creado desde cero con contratos. - `database/README.md`: V018..V024 agregadas (este commit final `ee36d86`). ## Artifacts engram - `sdd/prc-001-word-counter-spike/archive-report` — status ARCHIVED - `sdd/prc-001-word-counter-spike/verify-report-v2` — pass final - `sdd/prc-001-word-counter-spike/scope-delta-1` — historia del refactor post-smoke - `sdd/prc-001-word-counter-spike/apply-progress` — journey completo Closes #56 parcialmente (guards diferidos a FAC-001). No cierra #52..#55 ni #57/#58 (followups de PRC-001 standalone).
dmolinari added 1 commit 2026-04-21 16:06:05 +00:00
Archive follow-up: updates the migrations table in database/README.md with
V018 (PRD-002), V019 (PRD-003), and V020..V024 (PRC-001 + scope delta).
The table was stale since V017 (PRD-001). This entry keeps the onboarding
doc aligned with the actual migration chain applied on all three DBs.
dmolinari merged commit 0eab947975 into main 2026-04-21 16:06:20 +00:00
dmolinari deleted branch feature/PRC-001 2026-04-21 16:06:20 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#59