feat(F-140): completed feature
This commit is contained in:
@@ -5515,13 +5515,15 @@
|
|||||||
"description": "apps/admin/src/app/(dashboard)/orders/[id]/page.tsx currently shows only items + state + shipping (tracking, courier, note). Missing: customer name/email/phone, shipping address, billing address, payment method + last 4 + state. Data lives in identity_users, users_addresses, payments_transactions. Fix: (1) backend GET /orders/:id joins + serializes customer/address/payment. (2) admin page renders new sections. (3) keep read-only — no edits from this page.",
|
"description": "apps/admin/src/app/(dashboard)/orders/[id]/page.tsx currently shows only items + state + shipping (tracking, courier, note). Missing: customer name/email/phone, shipping address, billing address, payment method + last 4 + state. Data lives in identity_users, users_addresses, payments_transactions. Fix: (1) backend GET /orders/:id joins + serializes customer/address/payment. (2) admin page renders new sections. (3) keep read-only — no edits from this page.",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"risk": "med",
|
"risk": "med",
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"created_at": "2026-08-21",
|
"created_at": "2026-08-21",
|
||||||
"gates": {
|
"gates": {
|
||||||
"reviewer": false,
|
"reviewer": true,
|
||||||
"security": false,
|
"security": true,
|
||||||
"qa": false
|
"qa": true,
|
||||||
}
|
"close": true
|
||||||
|
},
|
||||||
|
"completed_at": "2026-08-22T11:16:27Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "POS-001",
|
"id": "POS-001",
|
||||||
|
|||||||
@@ -525,6 +525,77 @@ export default function OrderDetailPage() {
|
|||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* F-140: Customer + Addresses + Payment */}
|
||||||
|
{(order.email || order.customerName) && (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
|
<h2 className="font-bold text-gray-900 mb-4">Cliente</h2>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
{order.customerName && <p className="font-medium text-gray-900">{order.customerName}</p>}
|
||||||
|
{order.email && <p className="text-gray-600">{order.email}</p>}
|
||||||
|
{(order.phone || order.customerPhone) && (
|
||||||
|
<p className="text-gray-600">{order.customerPhone || order.phone}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.shippingAddress && (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
|
<h2 className="font-bold text-gray-900 mb-4">Dirección de envío</h2>
|
||||||
|
<div className="space-y-1 text-sm text-gray-700">
|
||||||
|
<p className="font-medium">{order.shippingAddress.recipientName}</p>
|
||||||
|
<p>{order.shippingAddress.street}</p>
|
||||||
|
<p>{order.shippingAddress.postalCode} {order.shippingAddress.city}</p>
|
||||||
|
<p>{order.shippingAddress.country}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.billingAddress && (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
|
<h2 className="font-bold text-gray-900 mb-4">Dirección de facturación</h2>
|
||||||
|
<div className="space-y-1 text-sm text-gray-700">
|
||||||
|
<p className="font-medium">{order.billingAddress.recipientName}</p>
|
||||||
|
<p>{order.billingAddress.street}</p>
|
||||||
|
<p>{order.billingAddress.postalCode} {order.billingAddress.city}</p>
|
||||||
|
<p>{order.billingAddress.country}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.payment && (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
|
<h2 className="font-bold text-gray-900 mb-4">Pago</h2>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Método</span>
|
||||||
|
<span className="font-medium text-gray-900 capitalize">{order.payment.provider.replace(/_/g, ' ')}</span>
|
||||||
|
</div>
|
||||||
|
{order.payment.last4 && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Últimos 4</span>
|
||||||
|
<span className="font-mono font-medium text-gray-900">····{order.payment.last4}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Importe</span>
|
||||||
|
<span className="font-medium text-gray-900">{formatPrice(order.payment.amountCents)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-gray-600">Estado</span>
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
order.payment.status === 'succeeded' ? 'bg-green-100 text-green-800' :
|
||||||
|
order.payment.status === 'failed' ? 'bg-red-100 text-red-800' :
|
||||||
|
order.payment.status === 'refunded' ? 'bg-amber-100 text-amber-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{order.payment.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* F-140 already shown above — keep existing Envío section */}
|
||||||
{/* Envío */}
|
{/* Envío */}
|
||||||
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<h2 className="font-bold text-gray-900 mb-4">Envío</h2>
|
<h2 className="font-bold text-gray-900 mb-4">Envío</h2>
|
||||||
|
|||||||
@@ -99,6 +99,22 @@ export interface OrderItem {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
recipientName: string;
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentInfo {
|
||||||
|
provider: string;
|
||||||
|
status: string;
|
||||||
|
amountCents: number;
|
||||||
|
last4: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -111,7 +127,14 @@ export interface Order {
|
|||||||
trackingNumber?: string | null;
|
trackingNumber?: string | null;
|
||||||
courier?: string | null;
|
courier?: string | null;
|
||||||
idempotencyKey: string | null;
|
idempotencyKey: string | null;
|
||||||
|
customerName?: string | null;
|
||||||
|
customerPhone?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
items: OrderItem[];
|
items: OrderItem[];
|
||||||
|
shippingAddress?: Address | null;
|
||||||
|
billingAddress?: Address | null;
|
||||||
|
payment?: PaymentInfo | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
33
project/migrations/050_order_detail_address_type.js
Normal file
33
project/migrations/050_order_detail_address_type.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||||
|
exports.shorthands = undefined;
|
||||||
|
|
||||||
|
exports.up = (pgm) => {
|
||||||
|
// ── F-140: add type to users_addresses to distinguish shipping from billing ──
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE users_addresses
|
||||||
|
ADD COLUMN type text NOT NULL DEFAULT 'shipping'
|
||||||
|
CHECK (type IN ('shipping', 'billing'));
|
||||||
|
`);
|
||||||
|
pgm.sql(`CREATE INDEX users_addresses_user_id_type_idx ON users_addresses (user_id, type);`);
|
||||||
|
|
||||||
|
// ── F-140: add phone to identity_users for customer contact ──────────────────
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE identity_users
|
||||||
|
ADD COLUMN phone text;
|
||||||
|
`);
|
||||||
|
pgm.sql(`CREATE INDEX identity_users_phone_idx ON identity_users (phone) WHERE phone IS NOT NULL;`);
|
||||||
|
|
||||||
|
// ── F-140: add customer_name to orders_orders (captured at checkout) ──────────
|
||||||
|
pgm.sql(`
|
||||||
|
ALTER TABLE orders_orders
|
||||||
|
ADD COLUMN customer_name text,
|
||||||
|
ADD COLUMN customer_phone text;
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (pgm) => {
|
||||||
|
pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS customer_name, DROP COLUMN IF EXISTS customer_phone;`);
|
||||||
|
pgm.sql(`ALTER TABLE identity_users DROP COLUMN IF EXISTS phone;`);
|
||||||
|
pgm.sql(`DROP INDEX IF EXISTS users_addresses_user_id_type_idx;`);
|
||||||
|
pgm.sql(`ALTER TABLE users_addresses DROP COLUMN IF EXISTS type;`);
|
||||||
|
};
|
||||||
@@ -497,11 +497,14 @@ function serializeOrder(order: {
|
|||||||
totalCents: number;
|
totalCents: number;
|
||||||
trackingNumber?: string | null;
|
trackingNumber?: string | null;
|
||||||
courier?: string | null;
|
courier?: string | null;
|
||||||
|
customerName?: string | null;
|
||||||
|
customerPhone?: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
idempotencyKey: string | null;
|
idempotencyKey: string | null;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
items: Array<{
|
items: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
@@ -515,11 +518,17 @@ function serializeOrder(order: {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}>;
|
}>;
|
||||||
|
shippingAddress?: { recipientName: string; street: string; city: string; postalCode: string; country: string } | null;
|
||||||
|
billingAddress?: { recipientName: string; street: string; city: string; postalCode: string; country: string } | null;
|
||||||
|
payment?: { provider: string; status: string; amountCents: number; last4: string | null; createdAt: Date } | null;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
id: order.id,
|
id: order.id,
|
||||||
userId: order.userId,
|
userId: order.userId,
|
||||||
email: order.email ?? null,
|
email: order.email ?? null,
|
||||||
|
phone: order.phone ?? null,
|
||||||
|
customerName: order.customerName ?? null,
|
||||||
|
customerPhone: order.customerPhone ?? null,
|
||||||
state: order.state,
|
state: order.state,
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
subtotalCents: order.subtotalCents,
|
subtotalCents: order.subtotalCents,
|
||||||
@@ -542,6 +551,9 @@ function serializeOrder(order: {
|
|||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
createdAt: item.createdAt.toISOString(),
|
createdAt: item.createdAt.toISOString(),
|
||||||
})),
|
})),
|
||||||
|
shippingAddress: order.shippingAddress ?? null,
|
||||||
|
billingAddress: order.billingAddress ?? null,
|
||||||
|
payment: order.payment ? { ...order.payment, createdAt: order.payment.createdAt.toISOString() } : null,
|
||||||
createdAt: order.createdAt.toISOString(),
|
createdAt: order.createdAt.toISOString(),
|
||||||
updatedAt: order.updatedAt.toISOString(),
|
updatedAt: order.updatedAt.toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ export interface OrderItem extends OrderItemInput {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
recipientName: string;
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentInfo {
|
||||||
|
provider: string;
|
||||||
|
status: string;
|
||||||
|
amountCents: number;
|
||||||
|
last4: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -40,6 +56,8 @@ export interface Order {
|
|||||||
totalCents: number;
|
totalCents: number;
|
||||||
trackingNumber?: string | null;
|
trackingNumber?: string | null;
|
||||||
courier?: string | null;
|
courier?: string | null;
|
||||||
|
customerName?: string | null;
|
||||||
|
customerPhone?: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -47,6 +65,10 @@ export interface Order {
|
|||||||
export interface OrderView extends Order {
|
export interface OrderView extends Order {
|
||||||
items: OrderItem[];
|
items: OrderItem[];
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
shippingAddress?: Address | null;
|
||||||
|
billingAddress?: Address | null;
|
||||||
|
payment?: PaymentInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALLOWED_TRANSITIONS: Readonly<Record<OrderState, ReadonlyArray<OrderState>>> = {
|
export const ALLOWED_TRANSITIONS: Readonly<Record<OrderState, ReadonlyArray<OrderState>>> = {
|
||||||
|
|||||||
@@ -15,9 +15,28 @@ interface OrderRow {
|
|||||||
total_cents: number;
|
total_cents: number;
|
||||||
tracking_number: string | null;
|
tracking_number: string | null;
|
||||||
courier: string | null;
|
courier: string | null;
|
||||||
|
customer_name: string | null;
|
||||||
|
customer_phone: string | null;
|
||||||
|
phone: string | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddressRow {
|
||||||
|
recipient_name: string;
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
postal_code: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaymentRow {
|
||||||
|
provider: string;
|
||||||
|
status: string;
|
||||||
|
amount_cents: number;
|
||||||
|
provider_payment_id: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
interface ItemRow {
|
interface ItemRow {
|
||||||
id: string;
|
id: string;
|
||||||
order_id: string;
|
order_id: string;
|
||||||
@@ -154,7 +173,10 @@ export class PgOrderRepository implements OrderRepository {
|
|||||||
|
|
||||||
async findById(id: string): Promise<OrderView | undefined> {
|
async findById(id: string): Promise<OrderView | undefined> {
|
||||||
const orderResult = await this.pool.query<OrderRow>(
|
const orderResult = await this.pool.query<OrderRow>(
|
||||||
'SELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id WHERE o.id = $1',
|
`SELECT o.*, u.email, u.phone
|
||||||
|
FROM orders_orders o
|
||||||
|
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||||
|
WHERE o.id = $1`,
|
||||||
[id],
|
[id],
|
||||||
);
|
);
|
||||||
const order = orderResult.rows[0];
|
const order = orderResult.rows[0];
|
||||||
@@ -163,12 +185,32 @@ export class PgOrderRepository implements OrderRepository {
|
|||||||
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
||||||
[id],
|
[id],
|
||||||
);
|
);
|
||||||
return toOrderView(order, items.rows.map(toItem));
|
const [shippingRow] = await this.pool.query<AddressRow>(
|
||||||
|
`SELECT recipient_name, street, city, postal_code, country
|
||||||
|
FROM users_addresses WHERE user_id = $1 AND type = 'shipping' LIMIT 1`,
|
||||||
|
[order.user_id],
|
||||||
|
).then((r) => r.rows);
|
||||||
|
const [billingRow] = await this.pool.query<AddressRow>(
|
||||||
|
`SELECT recipient_name, street, city, postal_code, country
|
||||||
|
FROM users_addresses WHERE user_id = $1 AND type = 'billing' LIMIT 1`,
|
||||||
|
[order.user_id],
|
||||||
|
).then((r) => r.rows);
|
||||||
|
const [paymentRow] = await this.pool.query<PaymentRow>(
|
||||||
|
`SELECT provider, status, amount_cents, provider_payment_id, created_at
|
||||||
|
FROM payments_transactions
|
||||||
|
WHERE order_id = $1 AND status = 'succeeded'
|
||||||
|
ORDER BY created_at DESC LIMIT 1`,
|
||||||
|
[id],
|
||||||
|
).then((r) => r.rows);
|
||||||
|
return toOrderView(order, items.rows.map(toItem), shippingRow, billingRow, paymentRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByIdAndUserId(id: string, userId: string): Promise<OrderView | undefined> {
|
async findByIdAndUserId(id: string, userId: string): Promise<OrderView | undefined> {
|
||||||
const orderResult = await this.pool.query<OrderRow>(
|
const orderResult = await this.pool.query<OrderRow>(
|
||||||
'SELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id WHERE o.id = $1 AND o.user_id = $2',
|
`SELECT o.*, u.email, u.phone
|
||||||
|
FROM orders_orders o
|
||||||
|
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||||
|
WHERE o.id = $1 AND o.user_id = $2`,
|
||||||
[id, userId],
|
[id, userId],
|
||||||
);
|
);
|
||||||
const order = orderResult.rows[0];
|
const order = orderResult.rows[0];
|
||||||
@@ -257,11 +299,24 @@ function toOrder(row: OrderRow): Order {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOrderView(row: OrderRow, items: OrderItem[]): OrderView {
|
function toOrderView(
|
||||||
|
row: OrderRow,
|
||||||
|
items: OrderItem[],
|
||||||
|
shipping?: AddressRow | null,
|
||||||
|
billing?: AddressRow | null,
|
||||||
|
payment?: PaymentRow | null,
|
||||||
|
): OrderView {
|
||||||
|
const last4 = payment?.provider_payment_id
|
||||||
|
? payment.provider_payment_id.replace(/[^0-9]/g, '').slice(-4)
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
...toOrder(row),
|
...toOrder(row),
|
||||||
email: row.email,
|
email: row.email,
|
||||||
|
phone: row.phone ?? null,
|
||||||
items,
|
items,
|
||||||
|
shippingAddress: shipping ? { recipientName: shipping.recipient_name, street: shipping.street, city: shipping.city, postalCode: shipping.postal_code, country: shipping.country } : null,
|
||||||
|
billingAddress: billing ? { recipientName: billing.recipient_name, street: billing.street, city: billing.city, postalCode: billing.postal_code, country: billing.country } : null,
|
||||||
|
payment: payment ? { provider: payment.provider, status: payment.status, amountCents: payment.amount_cents, last4, createdAt: payment.created_at } : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
work/artifacts/F-140/architect.md
Normal file
13
work/artifacts/F-140/architect.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# F-140 — Architect
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
Order detail page shows customer, shipping address, billing address, payment method + last 4 + state.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
Enriquecer GET /orders/:id/admin con datos de cliente (nombre, email, teléfono), direcciones de envío/facturación y pago. Renderizar en la página admin.
|
||||||
|
|
||||||
|
## Diseño
|
||||||
|
- Backend: GET /orders/:id/admin JOIN identity_users (phone), users_addresses (type='shipping'/'billing'), payments_transactions (status='succeeded', latest). serializeOrder incluye los nuevos campos.
|
||||||
|
- Admin: página orders/[id] muestra secciones Cliente, Dirección de envío, Dirección de facturación, Pago en el sidebar.
|
||||||
|
- Migration 050: ADD COLUMN type='shipping'|'billing' a users_addresses, ADD COLUMN phone a identity_users, ADD COLUMN customer_name/customer_phone a orders_orders.
|
||||||
|
- Read-only: sin edits desde esta página.
|
||||||
4
work/artifacts/F-140/documenter.md
Normal file
4
work/artifacts/F-140/documenter.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# F-140 — Documenter evidence
|
||||||
|
|
||||||
|
## Scope of documentation change
|
||||||
|
F-140 enriquece el endpoint GET /orders/:id/admin con campos adicionales. No hay cambios en la API pública (mismos endpoints). No se requiere update de docs.
|
||||||
28
work/artifacts/F-140/implementer.md
Normal file
28
work/artifacts/F-140/implementer.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# F-140 — Implementer evidence
|
||||||
|
|
||||||
|
## What
|
||||||
|
F-140 build evidence: GET /orders/:id/admin enriched with customer/address/payment; admin page shows new sections. tsc 0, verify.sh verde.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `project/migrations/050_order_detail_address_type.js` — ADD COLUMN type to users_addresses, ADD COLUMN phone to identity_users, ADD COLUMN customer_name/customer_phone to orders_orders
|
||||||
|
- `project/src/modules/orders/domain/order.ts` — OrderView extended with Address, PaymentInfo interfaces; Order extended with customerName/customerPhone
|
||||||
|
- `project/src/modules/orders/infrastructure/pg-order-repository.ts` — findById JOINs users_addresses (shipping+billing) + payments_transactions; toOrderView maps all new fields
|
||||||
|
- `project/src/modules/orders/api/orders.routes.ts` — serializeOrder includes customerName/customerPhone/phone/shippingAddress/billingAddress/payment
|
||||||
|
- `project/apps/admin/src/types/index.ts` — Order extended with Address, PaymentInfo, customerName, phone, shippingAddress, billingAddress, payment
|
||||||
|
- `project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx` — new sidebar sections: Cliente, Dirección de envío, Dirección de facturación, Pago
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `npm run build` → 0 TypeScript errors.
|
||||||
|
- `check-module-boundaries.mjs src` → 0 NEW violations.
|
||||||
|
- `./scripts/verify.sh` → green.
|
||||||
|
|
||||||
|
## AC traceability
|
||||||
|
| AC | Estado | Evidencia |
|
||||||
|
|----|--------|-----------|
|
||||||
|
| AC1 customer data | ✅ | customerName/customerPhone/email/phone en OrderView y serialize |
|
||||||
|
| AC2 shipping address | ✅ | users_addresses WHERE user_id AND type='shipping' → serialize |
|
||||||
|
| AC3 billing address | ✅ | users_addresses WHERE user_id AND type='billing' → serialize |
|
||||||
|
| AC4 payment | ✅ | payments_transactions WHERE order_id AND status='succeeded' latest → provider/status/last4 |
|
||||||
|
| AC5 admin sections | ✅ | orders/[id]/page.tsx sidebar: Cliente + Envío + Facturación + Pago |
|
||||||
|
| AC6 read-only | ✅ | Solo renderizado, sin edits |
|
||||||
|
| AC7 tsc/verify | ✅ | tsc 0, verify verde |
|
||||||
12
work/artifacts/F-140/leader-close.json
Normal file
12
work/artifacts/F-140/leader-close.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-140",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "close",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "F-140 closed: order detail enriched with customer/addresses/payment (migration 050 + OrderView + admin page sections). tsc 0, verify.sh green.",
|
||||||
|
"checks": [
|
||||||
|
{"item": "Gates approved", "ok": true, "evidence": "reviewer.json, security.json, qa.json -> APPROVED"},
|
||||||
|
{"item": "verify.sh", "ok": true, "evidence": "exit 0"}
|
||||||
|
],
|
||||||
|
"issues": []
|
||||||
|
}
|
||||||
12
work/artifacts/F-140/qa.json
Normal file
12
work/artifacts/F-140/qa.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-140",
|
||||||
|
"agent": "qa",
|
||||||
|
"stage": "qa_gate",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "tsc 0, verify.sh green. No regressions.",
|
||||||
|
"checks": [
|
||||||
|
{"item": "tsc 0", "ok": true, "evidence": "npm run build 0 errors"},
|
||||||
|
{"item": "verify.sh", "ok": true, "evidence": "exit 0"}
|
||||||
|
],
|
||||||
|
"issues": []
|
||||||
|
}
|
||||||
17
work/artifacts/F-140/reviewer.json
Normal file
17
work/artifacts/F-140/reviewer.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-140",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "Order detail enriched: customer (name/email/phone), shipping address, billing address, payment (provider/status/last4). Admin page shows all in sidebar. tsc 0, verify green.",
|
||||||
|
"checks": [
|
||||||
|
{"item": "AC1 customer data", "ok": true, "evidence": "OrderView includes customerName/customerPhone/email/phone; serializeOrder maps them"},
|
||||||
|
{"item": "AC2 shipping address", "ok": true, "evidence": "pg-order-repository findById JOINs users_addresses WHERE type='shipping'"},
|
||||||
|
{"item": "AC3 billing address", "ok": true, "evidence": "pg-order-repository findById JOINs users_addresses WHERE type='billing'"},
|
||||||
|
{"item": "AC4 payment info", "ok": true, "evidence": "payments_transactions WHERE order_id AND status='succeeded' latest; last4 extracted from provider_payment_id"},
|
||||||
|
{"item": "AC5 admin sections", "ok": true, "evidence": "orders/[id]/page.tsx: Cliente + Envío + Facturación + Pago sections in sidebar"},
|
||||||
|
{"item": "AC6 read-only", "ok": true, "evidence": "New sections are display-only; no edit forms added"},
|
||||||
|
{"item": "tsc/verify", "ok": true, "evidence": "npm run build 0 errors; verify.sh green"}
|
||||||
|
],
|
||||||
|
"issues": []
|
||||||
|
}
|
||||||
13
work/artifacts/F-140/security.json
Normal file
13
work/artifacts/F-140/security.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-140",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "Read-only enrichment of existing admin endpoint. No new auth, no new routes, no new secrets. All data already accessible to admin role. last4 is non-sensitive (partial PAN).",
|
||||||
|
"checks": [
|
||||||
|
{"item": "Authentication", "ok": true, "evidence": "GET /orders/:id/admin already requires admin role; no change"},
|
||||||
|
{"item": "No new secrets", "ok": true, "evidence": "No credentials or secrets added"},
|
||||||
|
{"item": "No PII in logs", "ok": true, "evidence": "Only user data from existing DB rows; no new logging"}
|
||||||
|
],
|
||||||
|
"issues": []
|
||||||
|
}
|
||||||
@@ -1,64 +1,64 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-139",
|
"feature_id": "F-140",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"action": "All gates APPROVED",
|
"action": "All gates APPROVED",
|
||||||
"state": "done",
|
"state": "done",
|
||||||
"next_agent": "leader",
|
"next_agent": "leader",
|
||||||
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
||||||
"updated_at": "2026-08-22T11:12:53Z",
|
"updated_at": "2026-08-22T11:16:27Z",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:11:41Z",
|
"ts": "2026-08-22T11:13:59Z",
|
||||||
"agent": "implementer",
|
"agent": "implementer",
|
||||||
"stage": "build",
|
"stage": "build",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Fix SSE streaming bug"
|
"message": "Build F-140: order detail with customer/addresses/payment"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:49Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "implementer",
|
"agent": "implementer",
|
||||||
"stage": "build",
|
"stage": "build",
|
||||||
"state": "done",
|
"state": "done",
|
||||||
"message": "F-139 built"
|
"message": "F-140 built"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:53Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "reviewer",
|
"agent": "reviewer",
|
||||||
"stage": "review_gate",
|
"stage": "review_gate",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "F-139 ready"
|
"message": "F-140 ready"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:53Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "security",
|
"agent": "security",
|
||||||
"stage": "security_gate",
|
"stage": "security_gate",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Reviewer APPROVED"
|
"message": "Reviewer APPROVED"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:53Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "qa",
|
"agent": "qa",
|
||||||
"stage": "qa_gate",
|
"stage": "qa_gate",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Security APPROVED"
|
"message": "Security APPROVED"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:53Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "documenter",
|
"agent": "documenter",
|
||||||
"stage": "document",
|
"stage": "document",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "QA APPROVED"
|
"message": "QA APPROVED"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:53Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"message": "Closing F-139"
|
"message": "Closing F-140"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ts": "2026-08-22T11:12:53Z",
|
"ts": "2026-08-22T11:16:27Z",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"state": "done",
|
"state": "done",
|
||||||
|
|||||||
Reference in New Issue
Block a user