Files
mercadodevida/project/src/modules/orders/application/order-service.ts
2026-08-21 12:27:13 +02:00

91 lines
3.4 KiB
TypeScript

import { OrderNotFoundError, OrderStateTransitionError } from '../domain/errors.js';
import type {
OrderEventPublisher,
OrderRepository,
OrderServicePort,
CreateOrderCommand,
} from '../domain/ports.js';
import { isTransitionAllowed, type OrderState, type OrderView, type OrderItemInput } from '../domain/order.js';
export class OrderService implements OrderServicePort {
constructor(
private readonly repo: OrderRepository,
private readonly events: OrderEventPublisher,
) {}
async create(input: CreateOrderCommand): Promise<OrderView> {
const order = await this.repo.create({
...input,
idempotencyKey: input.idempotencyKey ?? null,
});
await this.events.emit({ type: 'OrderCreated', orderId: order.id, userId: order.userId });
return order;
}
async listOrders(): Promise<OrderView[]> {
return this.repo.findAll();
}
async searchOrders(filters: {
state?: string;
q?: string;
limit: number;
offset: number;
}): Promise<{ items: OrderView[]; total: number }> {
if (!this.repo.search) {
const all = await this.repo.findAll();
return { items: all.slice(filters.offset, filters.offset + filters.limit), total: all.length };
}
return this.repo.search(filters);
}
/** Edición admin: reemplaza artículos y totales del pedido. */
async editItems(
id: string,
items: OrderItemInput[],
totals: { subtotalCents: number; discountCents: number; taxCents: number; totalCents: number },
): Promise<OrderView> {
const existing = await this.repo.findById(id);
if (!existing) throw new OrderNotFoundError();
if (!this.repo.replaceItems) throw new Error('Order repository does not support item editing');
const updated = await this.repo.replaceItems(id, items, totals);
if (!updated) throw new OrderNotFoundError();
return updated;
}
async transition(id: string, next: OrderState, userId: string): Promise<OrderView> {
const existing = await this.repo.findByIdAndUserId(id, userId);
if (!existing) throw new OrderNotFoundError();
if (!isTransitionAllowed(existing.state, next))
throw new OrderStateTransitionError(existing.state, next);
const updated = await this.repo.updateState(id, next);
if (!updated) throw new OrderNotFoundError();
if (next === 'PAID') await this.events.emit({ type: 'OrderPaid', orderId: id, userId });
if (next === 'CANCELLED')
await this.events.emit({ type: 'OrderCancelled', orderId: id, userId });
return updated;
}
async getOrder(id: string, userId: string): Promise<OrderView | undefined> {
return this.repo.findByIdAndUserId(id, userId);
}
async getOrderAdmin(id: string): Promise<OrderView | undefined> {
return this.repo.findById(id);
}
async transitionAdmin(id: string, next: OrderState, trackingNumber?: string, courier?: string): Promise<OrderView> {
const existing = await this.repo.findById(id);
if (!existing) throw new OrderNotFoundError();
if (!isTransitionAllowed(existing.state, next))
throw new OrderStateTransitionError(existing.state, next);
const updated = await this.repo.updateState(id, next, trackingNumber, courier);
if (!updated) throw new OrderNotFoundError();
if (next === 'PAID')
await this.events.emit({ type: 'OrderPaid', orderId: id, userId: existing.userId });
if (next === 'CANCELLED')
await this.events.emit({ type: 'OrderCancelled', orderId: id, userId: existing.userId });
return updated;
}
}