feat(F-153): completed feature
This commit is contained in:
@@ -262,11 +262,8 @@ export async function registerOrdersRoutes(
|
||||
let notified = false;
|
||||
let notificationError = '';
|
||||
try {
|
||||
const emailRow = await deps.pool.query<{ email: string }>(
|
||||
'SELECT email FROM identity_users WHERE id = $1',
|
||||
[order.userId],
|
||||
);
|
||||
const to = emailRow.rows[0]?.email;
|
||||
// F-153: email is now associated on the order view via a repository join.
|
||||
const to = order.email;
|
||||
if (to) {
|
||||
await sendOrderStatusEmail(deps.pool, {
|
||||
to,
|
||||
@@ -504,6 +501,7 @@ function serializeOrder(order: {
|
||||
updatedAt: Date;
|
||||
idempotencyKey: string | null;
|
||||
userId?: string;
|
||||
email?: string | null;
|
||||
items: Array<{
|
||||
id: string;
|
||||
productId: string;
|
||||
@@ -521,6 +519,7 @@ function serializeOrder(order: {
|
||||
return {
|
||||
id: order.id,
|
||||
userId: order.userId,
|
||||
email: order.email ?? null,
|
||||
state: order.state,
|
||||
currency: order.currency,
|
||||
subtotalCents: order.subtotalCents,
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface Order {
|
||||
|
||||
export interface OrderView extends Order {
|
||||
items: OrderItem[];
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export const ALLOWED_TRANSITIONS: Readonly<Record<OrderState, ReadonlyArray<OrderState>>> = {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { PgOrderRepository } from './pg-order-repository.js';
|
||||
import type { OrderView } from '../domain/order.js';
|
||||
|
||||
/** F-153: mock mínimo de pg.Pool que discrimina por SQL (ordenes vs items). */
|
||||
function mockPool(orderRow: Record<string, unknown> | null, itemRows: unknown[] = []) {
|
||||
const query = vi.fn((sql: string) => {
|
||||
const text = String(sql);
|
||||
if (text.includes('orders_items')) return Promise.resolve({ rows: itemRows });
|
||||
return Promise.resolve({ rows: orderRow ? [orderRow] : [] });
|
||||
});
|
||||
const pool = { query } as unknown as import('pg').Pool;
|
||||
return { pool, query };
|
||||
}
|
||||
|
||||
const BASE_ORDER = {
|
||||
id: 'order-1',
|
||||
user_id: 'user-1',
|
||||
idempotency_key: null,
|
||||
state: 'PENDING',
|
||||
currency: 'EUR',
|
||||
subtotal_cents: 1000,
|
||||
discount_cents: 0,
|
||||
tax_cents: 210,
|
||||
total_cents: 1210,
|
||||
tracking_number: null,
|
||||
courier: null,
|
||||
created_at: new Date('2026-01-01T00:00:00Z'),
|
||||
updated_at: new Date('2026-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
describe('PgOrderRepository.findById — F-153 customer email association', () => {
|
||||
it('resolves customer email through the identity_users join', async () => {
|
||||
const { pool, query } = mockPool({ ...BASE_ORDER, email: 'customer@example.com' });
|
||||
const repo = new PgOrderRepository(pool);
|
||||
|
||||
const result: OrderView | undefined = await repo.findById('order-1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.email).toBe('customer@example.com');
|
||||
const orderQuery = String(query.mock.calls[0]![0]);
|
||||
expect(orderQuery).toContain('identity_users');
|
||||
expect(orderQuery).toContain('u.email');
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const result = await repo.findById('order-1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.email).toBeNull();
|
||||
});
|
||||
|
||||
it('returns undefined when the order does not exist', async () => {
|
||||
const { pool } = mockPool(null);
|
||||
const repo = new PgOrderRepository(pool);
|
||||
|
||||
const result = await repo.findById('missing');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import type { Order, OrderItem, OrderItemInput, OrderState, OrderView } from '..
|
||||
interface OrderRow {
|
||||
id: string;
|
||||
user_id: string;
|
||||
email: string | null;
|
||||
idempotency_key: string | null;
|
||||
state: OrderState;
|
||||
currency: 'EUR';
|
||||
@@ -92,7 +93,7 @@ export class PgOrderRepository implements OrderRepository {
|
||||
|
||||
async findAll(): Promise<OrderView[]> {
|
||||
const result = await this.pool.query<OrderRow>(
|
||||
'SELECT * FROM orders_orders ORDER BY created_at DESC',
|
||||
'SELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id ORDER BY o.created_at DESC',
|
||||
);
|
||||
return Promise.all(
|
||||
result.rows.map(async (order) => {
|
||||
@@ -100,7 +101,7 @@ export class PgOrderRepository implements OrderRepository {
|
||||
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY id',
|
||||
[order.id],
|
||||
);
|
||||
return { ...toOrder(order), items: items.rows.map(toItem) };
|
||||
return toOrderView(order, items.rows.map(toItem));
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -132,7 +133,7 @@ export class PgOrderRepository implements OrderRepository {
|
||||
const total = Number(countResult.rows[0]?.count ?? '0');
|
||||
values.push(filters.limit, filters.offset);
|
||||
const result = await this.pool.query<OrderRow>(
|
||||
`SELECT o.* FROM orders_orders o
|
||||
`SELECT o.*, u.email FROM orders_orders o
|
||||
LEFT JOIN identity_users u ON u.id = o.user_id
|
||||
${whereSql}
|
||||
ORDER BY o.created_at DESC
|
||||
@@ -145,7 +146,7 @@ export class PgOrderRepository implements OrderRepository {
|
||||
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY id',
|
||||
[order.id],
|
||||
);
|
||||
return { ...toOrder(order), items: itemResult.rows.map(toItem) };
|
||||
return toOrderView(order, itemResult.rows.map(toItem));
|
||||
}),
|
||||
);
|
||||
return { items, total };
|
||||
@@ -153,7 +154,7 @@ export class PgOrderRepository implements OrderRepository {
|
||||
|
||||
async findById(id: string): Promise<OrderView | undefined> {
|
||||
const orderResult = await this.pool.query<OrderRow>(
|
||||
'SELECT * FROM orders_orders WHERE id = $1',
|
||||
'SELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id WHERE o.id = $1',
|
||||
[id],
|
||||
);
|
||||
const order = orderResult.rows[0];
|
||||
@@ -162,12 +163,12 @@ export class PgOrderRepository implements OrderRepository {
|
||||
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
||||
[id],
|
||||
);
|
||||
return { ...toOrder(order), items: items.rows.map(toItem) };
|
||||
return toOrderView(order, items.rows.map(toItem));
|
||||
}
|
||||
|
||||
async findByIdAndUserId(id: string, userId: string): Promise<OrderView | undefined> {
|
||||
const orderResult = await this.pool.query<OrderRow>(
|
||||
'SELECT * FROM orders_orders WHERE id = $1 AND user_id = $2',
|
||||
'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',
|
||||
[id, userId],
|
||||
);
|
||||
const order = orderResult.rows[0];
|
||||
@@ -176,7 +177,7 @@ export class PgOrderRepository implements OrderRepository {
|
||||
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
||||
[id],
|
||||
);
|
||||
return { ...toOrder(order), items: items.rows.map(toItem) };
|
||||
return toOrderView(order, items.rows.map(toItem));
|
||||
}
|
||||
|
||||
async updateState(id: string, state: OrderState, trackingNumber?: string, courier?: string): Promise<OrderView | undefined> {
|
||||
@@ -256,6 +257,14 @@ function toOrder(row: OrderRow): Order {
|
||||
};
|
||||
}
|
||||
|
||||
function toOrderView(row: OrderRow, items: OrderItem[]): OrderView {
|
||||
return {
|
||||
...toOrder(row),
|
||||
email: row.email,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
function toItem(row: ItemRow): OrderItem {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
Reference in New Issue
Block a user