91 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|