feat(club-003): club POS integration: identify member and register cashback from sales
This commit is contained in:
34
work/artifacts/CLUB-003/security.json
Normal file
34
work/artifacts/CLUB-003/security.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"feature_id": "CLUB-003",
|
||||
"agent": "security",
|
||||
"stage": "security_gate",
|
||||
"verdict": "APPROVED",
|
||||
"security_check": "security",
|
||||
"summary": "Aprobado: la integración Club/TPV usa consultas parametrizadas, validación UUID en la API POS, y fail-safe para evitar que errores del ledger bloqueen ventas.",
|
||||
"checks": {
|
||||
"auth_pos_sale": "OK: POST /pos/sales sigue requiriendo authenticate + requireAnyRole(['admin','pos_manager','pos_cashier']); clubMemberId no es un campo privileged sino un identificador de socio que el cajero introduce en la caja.",
|
||||
"input_validation": "OK: clubMemberId se valida como UUID en el schema Zod de pos.routes.ts; memberCode en /club/resolve se normaliza con trim() y se verifica como string no vacío antes de buscar.",
|
||||
"sql_injection": "OK: todas las consultas SQL usan placeholders ($1, $2…); la idempotency_key del ledger se compone de club-earn-${orderId} donde orderId es un UUID del order ya existente.",
|
||||
"ledger_integrity": "OK: INSERT INTO club_transactions usa ON CONFLICT (idempotency_key) DO NOTHING; si la clave ya existe por una escritura anterior, no se duplica la acumulación.",
|
||||
"fail_safe": "OK: el bloque de cashback está envuelto en try/catch; si la escritura del ledger falla, la venta continúa y se devuelve sin clubEarnedCents.",
|
||||
"integer_overflow": "OK: Math.round((totalCents * cashbackBps) / 10_000) es una operación de punto flotante redondeada; el resultado se inserta como integer en balance_delta_cents."
|
||||
},
|
||||
"notes": [
|
||||
"GET /club/resolve es público (sin auth) porque el TPV necesita resolver códigos Club sin sesión backoffice; el código de socio es un identificador público que no expone secretos.",
|
||||
"El cashback no puede ser negativo porque se calcula solo para ventas COMPLETED con cashbackBps >= 0.",
|
||||
"loadResult recupera clubEarnedCents del ledger por sale_id para mantener consistencia con la fuente de verdad."
|
||||
],
|
||||
"evidence": [
|
||||
"project/src/modules/club/api/club.routes.ts",
|
||||
"project/src/modules/club/application/club-service.ts",
|
||||
"project/src/modules/club/infrastructure/pg-club-repository.ts",
|
||||
"project/src/modules/pos/application/create-pos-sale.ts",
|
||||
"project/src/modules/pos/domain/pos-sale.ts",
|
||||
"rg -n \"clubMemberId|clubEarnedCents|findMemberByCode|resolveByCode|/club/resolve\" project/src/modules/club project/src/modules/pos/application/create-pos-sale.ts",
|
||||
"cd project && npm run typecheck",
|
||||
"cd project && npm run build",
|
||||
"cd project/apps/pos && npm run build",
|
||||
"./scripts/verify.sh"
|
||||
],
|
||||
"timestamp": "2026-08-26T18:15:25Z"
|
||||
}
|
||||
Reference in New Issue
Block a user