feat(F-161): completed feature
This commit is contained in:
@@ -526,46 +526,55 @@ export default function OrderDetailPage() {
|
||||
{/* Sidebar */}
|
||||
<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 className="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h2 className="font-bold text-gray-900 mb-4">Cliente</h2>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div>
|
||||
<dt className="text-xs font-semibold uppercase tracking-wide text-gray-400">Nombre</dt>
|
||||
<dd className="font-medium text-gray-900">{order.customerName || order.shippingAddress?.recipientName || 'No registrado'}</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<dt className="text-xs font-semibold uppercase tracking-wide text-gray-400">Email</dt>
|
||||
<dd className={order.email ? 'text-gray-700' : 'text-amber-700'}>{order.email || 'No registrado'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-semibold uppercase tracking-wide text-gray-400">Teléfono</dt>
|
||||
<dd className="text-gray-700">{order.customerPhone || order.phone || 'No registrado'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</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="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>
|
||||
{order.shippingAddress ? (
|
||||
<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>
|
||||
)}
|
||||
) : (
|
||||
<p className="text-sm text-amber-700">No registrada para este pedido.</p>
|
||||
)}
|
||||
</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="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>
|
||||
{order.billingAddress ? (
|
||||
<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>
|
||||
)}
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No registrada; consultar la dirección de envío.</p>
|
||||
)}
|
||||
</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="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h2 className="font-bold text-gray-900 mb-4">Pago</h2>
|
||||
{order.payment ? (
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Método</span>
|
||||
@@ -593,8 +602,10 @@ export default function OrderDetailPage() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No hay una transacción de pago registrada para este pedido.</p>
|
||||
)}
|
||||
</div>
|
||||
{/* F-140 already shown above — keep existing Envío section */}
|
||||
{/* Envío */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||
|
||||
@@ -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<import('@/types').Order>(`/api/orders/${id}`),
|
||||
get: (id: string) => api.get<import('@/types').Order>(`/api/orders/${id}/admin`),
|
||||
transition: (id: string, state: string, trackingNumber?: string, courier?: string) =>
|
||||
api.post<import('@/types').Order & { notified?: boolean; notificationError?: string }>(
|
||||
`/api/orders/${id}/transitions/admin`,
|
||||
|
||||
@@ -342,7 +342,9 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
||||
await app.register(async (instance) => {
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -173,9 +173,13 @@ export class PgOrderRepository implements OrderRepository {
|
||||
|
||||
async findById(id: string): Promise<OrderView | undefined> {
|
||||
const orderResult = await this.pool.query<OrderRow>(
|
||||
`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<OrderView | undefined> {
|
||||
const orderResult = await this.pool.query<OrderRow>(
|
||||
`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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user