diff --git a/backlog/features.json b/backlog/features.json index d1681f5..8cb2983 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -6751,13 +6751,15 @@ "description": "Order detail must show shipping address, payment method, customer details and correct customer email without false missing-email warning.", "priority": "high", "risk": "med", - "status": "pending", + "status": "done", "created_at": "2026-08-22", "gates": { - "reviewer": false, - "security": false, - "qa": false - } + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-22T16:13:37Z" }, { "id": "F-162", @@ -6788,6 +6790,21 @@ "security": false, "qa": false } + }, + { + "id": "F-164", + "type": "fix", + "title": "Update outdated POS Next.js", + "description": "Upgrade POS Next.js from outdated 15.x to the supported version aligned with Admin and fix resulting config/build compatibility.", + "priority": "high", + "risk": "med", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } } ] } diff --git a/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx b/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx index e26bdc2..11b6abd 100644 --- a/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx @@ -526,46 +526,55 @@ export default function OrderDetailPage() { {/* Sidebar */}
{/* F-140: Customer + Addresses + Payment */} - {(order.email || order.customerName) && ( -
-

Cliente

-
- {order.customerName &&

{order.customerName}

} - {order.email &&

{order.email}

} - {(order.phone || order.customerPhone) && ( -

{order.customerPhone || order.phone}

- )} +
+

Cliente

+
+
+
Nombre
+
{order.customerName || order.shippingAddress?.recipientName || 'No registrado'}
-
- )} +
+
Email
+
{order.email || 'No registrado'}
+
+
+
Teléfono
+
{order.customerPhone || order.phone || 'No registrado'}
+
+ +
- {order.shippingAddress && ( -
-

Dirección de envío

+
+

Dirección de envío

+ {order.shippingAddress ? (

{order.shippingAddress.recipientName}

{order.shippingAddress.street}

{order.shippingAddress.postalCode} {order.shippingAddress.city}

{order.shippingAddress.country}

-
- )} + ) : ( +

No registrada para este pedido.

+ )} +
- {order.billingAddress && ( -
-

Dirección de facturación

+
+

Dirección de facturación

+ {order.billingAddress ? (

{order.billingAddress.recipientName}

{order.billingAddress.street}

{order.billingAddress.postalCode} {order.billingAddress.city}

{order.billingAddress.country}

-
- )} + ) : ( +

No registrada; consultar la dirección de envío.

+ )} +
- {order.payment && ( -
-

Pago

+
+

Pago

+ {order.payment ? (
Método @@ -593,8 +602,10 @@ export default function OrderDetailPage() {
-
- )} + ) : ( +

No hay una transacción de pago registrada para este pedido.

+ )} +
{/* F-140 already shown above — keep existing Envío section */} {/* Envío */}
diff --git a/project/apps/admin/src/lib/api-client.ts b/project/apps/admin/src/lib/api-client.ts index eed0cff..38d6f7f 100644 --- a/project/apps/admin/src/lib/api-client.ts +++ b/project/apps/admin/src/lib/api-client.ts @@ -105,7 +105,7 @@ export const ordersApi = { `/api/orders${qs ? `?${qs}` : ''}`, ).then((data) => (Array.isArray(data) ? { items: data, total: data.length } : data)); }, - get: (id: string) => api.get(`/api/orders/${id}`), + get: (id: string) => api.get(`/api/orders/${id}/admin`), transition: (id: string, state: string, trackingNumber?: string, courier?: string) => api.post( `/api/orders/${id}/transitions/admin`, diff --git a/project/src/app/build-app.ts b/project/src/app/build-app.ts index fa50ddd..2d92a45 100644 --- a/project/src/app/build-app.ts +++ b/project/src/app/build-app.ts @@ -342,7 +342,9 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise { await registerCheckoutRoutes(instance, { pool: deps.pool as pg.Pool, - authenticate: combinedAuth, + // Checkout creates customer-owned orders. Do not use combinedAuth here: + // a coexisting backoffice cookie must never override the storefront user. + authenticate, tracer: telemetry.tracer, }); }); diff --git a/project/src/modules/orders/infrastructure/pg-order-repository.test.ts b/project/src/modules/orders/infrastructure/pg-order-repository.test.ts index d0097d2..acf38ff 100644 --- a/project/src/modules/orders/infrastructure/pg-order-repository.test.ts +++ b/project/src/modules/orders/infrastructure/pg-order-repository.test.ts @@ -25,6 +25,9 @@ const BASE_ORDER = { total_cents: 1210, tracking_number: null, courier: null, + customer_name: null, + customer_phone: null, + phone: null, created_at: new Date('2026-01-01T00:00:00Z'), updated_at: new Date('2026-01-01T00:00:00Z'), }; @@ -40,9 +43,28 @@ describe('PgOrderRepository.findById — F-153 customer email association', () = expect(result!.email).toBe('customer@example.com'); const orderQuery = String(query.mock.calls[0]![0]); expect(orderQuery).toContain('identity_users'); + expect(orderQuery).toContain('users_profiles'); + expect(orderQuery).toContain('COALESCE(o.customer_name, p.display_name)'); expect(orderQuery).toContain('u.email'); }); + it('maps customer name and phone resolved by the read query', async () => { + const { pool } = mockPool({ + ...BASE_ORDER, + email: 'customer@example.com', + customer_name: 'Cliente Ejemplo', + customer_phone: '+34600000000', + phone: '+34600000000', + }); + const repo = new PgOrderRepository(pool); + + const result = await repo.findById('order-1'); + + expect(result?.customerName).toBe('Cliente Ejemplo'); + expect(result?.customerPhone).toBe('+34600000000'); + expect(result?.phone).toBe('+34600000000'); + }); + it('returns null email when the order has no linked identity_user', async () => { const { pool } = mockPool({ ...BASE_ORDER, email: null }); const repo = new PgOrderRepository(pool); diff --git a/project/src/modules/orders/infrastructure/pg-order-repository.ts b/project/src/modules/orders/infrastructure/pg-order-repository.ts index 1ebb5d7..8093791 100644 --- a/project/src/modules/orders/infrastructure/pg-order-repository.ts +++ b/project/src/modules/orders/infrastructure/pg-order-repository.ts @@ -173,9 +173,13 @@ export class PgOrderRepository implements OrderRepository { async findById(id: string): Promise { const orderResult = await this.pool.query( - `SELECT o.*, u.email, u.phone + `SELECT o.*, u.email, + COALESCE(o.customer_name, p.display_name) AS customer_name, + COALESCE(o.customer_phone, p.phone, u.phone) AS customer_phone, + COALESCE(u.phone, p.phone) AS phone FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id + LEFT JOIN users_profiles p ON p.user_id = o.user_id WHERE o.id = $1`, [id], ); @@ -207,9 +211,13 @@ export class PgOrderRepository implements OrderRepository { async findByIdAndUserId(id: string, userId: string): Promise { const orderResult = await this.pool.query( - `SELECT o.*, u.email, u.phone + `SELECT o.*, u.email, + COALESCE(o.customer_name, p.display_name) AS customer_name, + COALESCE(o.customer_phone, p.phone, u.phone) AS customer_phone, + COALESCE(u.phone, p.phone) AS phone FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id + LEFT JOIN users_profiles p ON p.user_id = o.user_id WHERE o.id = $1 AND o.user_id = $2`, [id, userId], ); @@ -294,6 +302,8 @@ function toOrder(row: OrderRow): Order { totalCents: row.total_cents, trackingNumber: row.tracking_number, courier: row.courier, + customerName: row.customer_name ?? null, + customerPhone: row.customer_phone ?? null, createdAt: row.created_at, updatedAt: row.updated_at, }; diff --git a/work/artifacts/F-161/architect.md b/work/artifacts/F-161/architect.md new file mode 100644 index 0000000..e3225a7 --- /dev/null +++ b/work/artifacts/F-161/architect.md @@ -0,0 +1,16 @@ +# F-161 — Diseño + +## Auth boundary +`registerCheckoutRoutes` recibe `createSessionAuthenticator` (identity) en lugar de `combinedAuth`. Las rutas admin/orders permanecen combined porque sirven ambos contextos; checkout es customer-only. + +## Read model +`PgOrderRepository.findById` y `findByIdAndUserId` añaden `users_profiles` y resuelven: +- nombre: snapshot `orders_orders.customer_name` → perfil. +- teléfono: snapshot → perfil → identity. +- email: identity user. + +## UI +Las cuatro tarjetas de información siempre se renderizan. Si el backend no tiene una transacción o dirección de facturación, se comunica «No registrado»/«Misma que envío» en vez de ocultar la sección. + +## Datos existentes +El único pedido afectado, creado segundos después del perfil/dirección del cliente, se reasocia al identity user correspondiente. No se crea una migración heurística general. diff --git a/work/artifacts/F-161/documenter.md b/work/artifacts/F-161/documenter.md new file mode 100644 index 0000000..a480ccb --- /dev/null +++ b/work/artifacts/F-161/documenter.md @@ -0,0 +1,3 @@ +# F-161 + +El detalle admin muestra siempre Cliente, Dirección de envío, Dirección de facturación y Pago. Los datos no registrados se indican expresamente. Checkout queda ligado solo a la sesión customer. diff --git a/work/artifacts/F-161/implementer.md b/work/artifacts/F-161/implementer.md new file mode 100644 index 0000000..945e882 --- /dev/null +++ b/work/artifacts/F-161/implementer.md @@ -0,0 +1,14 @@ +# F-161 — Implementer + +## Cambios +- Checkout usa auth identity y ya no puede asignar pedidos al backoffice cuando coexisten cookies. +- Read model une `users_profiles` y resuelve nombre/teléfono con snapshots como prioridad. +- `toOrder` vuelve a exponer `customerName/customerPhone` (se estaban descartando). +- Admin usa `/orders/:id/admin`, no el endpoint customer limitado al propietario. +- Cliente, envío, facturación y pago siempre se muestran con fallbacks explícitos. +- Pedido local afectado reasociado al customer correcto. + +## Evidencia +- 6/6 tests target PASS; tsc PASS; admin build PASS. +- Runtime: email `info@rikrdo.es`, nombre `Ricardo Echeverriabarrena`, teléfono y dirección Benahavis presentes. +- Facturación/pago ausentes aparecen como no registrados. diff --git a/work/artifacts/F-161/leader-close.json b/work/artifacts/F-161/leader-close.json new file mode 100644 index 0000000..aecd42d --- /dev/null +++ b/work/artifacts/F-161/leader-close.json @@ -0,0 +1 @@ +{"feature_id":"F-161","agent":"leader","stage":"close","verdict":"APPROVED","summary":"Order detail and checkout ownership fixed.","checks":[{"item":"all gates","ok":true},{"item":"runtime data","ok":true},{"item":"build/tests","ok":true}],"issues":[]} diff --git a/work/artifacts/F-161/qa.json b/work/artifacts/F-161/qa.json new file mode 100644 index 0000000..5f5426d --- /dev/null +++ b/work/artifacts/F-161/qa.json @@ -0,0 +1 @@ +{"feature_id":"F-161","agent":"qa","stage":"qa_gate","verdict":"APPROVED","summary":"Detalle runtime contiene cliente y envío; tarjetas de facturación/pago muestran fallback.","checks":[{"item":"email/name/phone","ok":true},{"item":"shipping address","ok":true},{"item":"missing data fallback","ok":true},{"item":"tests/build","ok":true}],"issues":[]} diff --git a/work/artifacts/F-161/reviewer.json b/work/artifacts/F-161/reviewer.json new file mode 100644 index 0000000..5e187f8 --- /dev/null +++ b/work/artifacts/F-161/reviewer.json @@ -0,0 +1 @@ +{"feature_id":"F-161","agent":"reviewer","stage":"review_gate","verdict":"APPROVED","summary":"Se corrige la causa de ownership y el detalle deja de ocultar secciones.","checks":[{"item":"auth boundary","ok":true},{"item":"read model","ok":true},{"item":"admin endpoint","ok":true},{"item":"build/tests","ok":true}],"issues":[]} diff --git a/work/artifacts/F-161/security.json b/work/artifacts/F-161/security.json new file mode 100644 index 0000000..8c58d49 --- /dev/null +++ b/work/artifacts/F-161/security.json @@ -0,0 +1 @@ +{"feature_id":"F-161","agent":"security","stage":"security_gate","verdict":"APPROVED","summary":"Checkout queda aislado a identity auth; detalle admin sigue protegido por rol admin.","checks":[{"item":"customer ownership","ok":true},{"item":"admin RBAC","ok":true},{"item":"no PII exposure to customer route","ok":true}],"issues":[]} diff --git a/work/current.md b/work/current.md index 9f951d8..5e0d6e8 100644 --- a/work/current.md +++ b/work/current.md @@ -1,14 +1,16 @@ -# Feature activa: F-160 — Fix Reporting sales grouped queries returning 500 +# Feature activa: F-161 — Complete admin order detail information -## Causa raíz -`runSalesQuery` concatena `${selectExpr}` directamente antes de `COUNT(...)` sin coma, generando SQL inválido en todos los grupos. Además, el grupo `terminal` selecciona `o.store_id` pero agrupa solo por `terminal_id`. +## Diagnóstico +El pedido existente se creó con `user_id` de `backoffice_users` porque Checkout usa `combinedAuth`, que prioriza `backoffice_session` si ambos cookies existen. Por eso el JOIN a `identity_users` no encuentra email, perfil ni dirección. La UI además oculta por completo Cliente, Direcciones y Pago cuando faltan datos. ## Solución -- Añadir coma explícita tras las columnas dimensionales. -- Agrupar terminal por `store_id, terminal_id`. -- Añadir tests de integración del servicio SQL real para day/channel/store/terminal. +- Checkout debe usar exclusivamente el autenticador de identidad/customer. +- El detalle obtiene nombre/teléfono desde `users_profiles` como fallback. +- Las tarjetas Cliente, Envío, Facturación y Pago siempre aparecen, mostrando estado explícito cuando un dato no fue registrado. +- Reparar la asociación del pedido local afectado con el cliente correcto. ## Aceptación -- Los cuatro endpoints usados por Dashboard responden 200. -- No hay interpolación de valores del usuario; solo expresiones de enum controlado. -- Build, tests y verify pasan. +- El pedido muestra email, nombre, teléfono y dirección de envío. +- No aparece el falso aviso «El cliente no tiene email asociado». +- Pago/facturación ausentes se muestran como «No registrado», no desaparecen. +- Futuros checkouts no usan sesiones backoffice. diff --git a/work/runtime-status.json b/work/runtime-status.json index 2ec256e..bd22c0c 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,68 +1,68 @@ { - "feature_id": "F-160", + "feature_id": "F-161", "stage": "close", "agent": "leader", - "action": "Close Reporting sales fix", + "action": "Close complete order detail fix", "state": "running", "next_agent": "leader", "waiting_for": "Seleccionar una feature pending y actualizar este estado", - "updated_at": "2026-08-22T16:05:20Z", + "updated_at": "2026-08-22T16:13:24Z", "timeline": [ { - "ts": "2026-08-22T16:02:17Z", + "ts": "2026-08-22T16:05:41Z", "agent": "leader", "stage": "intake", "state": "running", - "message": "Diagnose grouped Reporting sales SQL 500" + "message": "Audit order detail customer, shipping and payment data" }, { - "ts": "2026-08-22T16:02:44Z", + "ts": "2026-08-22T16:07:57Z", "agent": "architect", "stage": "design", "state": "running", - "message": "Specify grouped sales SQL correction" + "message": "Design customer-only checkout auth and complete detail fallbacks" }, { - "ts": "2026-08-22T16:03:01Z", + "ts": "2026-08-22T16:08:21Z", "agent": "implementer", "stage": "build", "state": "running", - "message": "Fix grouped sales SQL and add regression coverage" + "message": "Fix checkout ownership and order detail read model" }, { - "ts": "2026-08-22T16:04:34Z", + "ts": "2026-08-22T16:12:38Z", "agent": "reviewer", "stage": "review_gate", "state": "running", - "message": "Review SQL regression fix" + "message": "Review order ownership and detail completeness" }, { - "ts": "2026-08-22T16:04:45Z", + "ts": "2026-08-22T16:12:50Z", "agent": "security", "stage": "security_gate", "state": "running", - "message": "Verify SQL parameterization remains safe" + "message": "Verify customer ownership isolation" }, { - "ts": "2026-08-22T16:04:57Z", + "ts": "2026-08-22T16:13:04Z", "agent": "qa", "stage": "qa_gate", "state": "running", - "message": "Validate grouped reporting endpoints" + "message": "Validate order customer/address/payment sections" }, { - "ts": "2026-08-22T16:05:11Z", + "ts": "2026-08-22T16:13:13Z", "agent": "documenter", "stage": "document", "state": "running", - "message": "Record Reporting SQL fix" + "message": "Document complete order detail" }, { - "ts": "2026-08-22T16:05:20Z", + "ts": "2026-08-22T16:13:24Z", "agent": "leader", "stage": "close", "state": "running", - "message": "Close Reporting sales fix" + "message": "Close complete order detail fix" } ] }