import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../../infrastructure/database/database.service';
import { RealtimePublisherService } from '../../../infrastructure/realtime/realtime-publisher.service';
import { DomainConflictError } from '../../../common/errors/domain-conflict.error';
@Injectable()
export class KitchenOrderChangeService {
constructor(
private readonly database: DatabaseService,
private readonly realtime: RealtimePublisherService,
) {}
async applyOrderItemVoided(input: {
organizationId: string;
locationId: string;
orderItemId: string;
}) {
this.assertScope(input, input.orderItemId, 'kitchen.order_change_invalid');
const rows = await this.database.query<{ id: string; kitchen_ticket_id: string }>(
`update kitchen_ticket_items kti
set status='cancelled', updated_at=now()
from kitchen_tickets kt
where kti.kitchen_ticket_id=kt.id
and kti.order_item_id=$1
and kt.organization_id=$2
and kt.location_id=$3
and kti.status not in ('bumped','cancelled')
returning kti.id, kti.kitchen_ticket_id`,
[input.orderItemId, input.organizationId, input.locationId],
);
for (const row of rows) {
await this.database.query(
`insert into kitchen_ticket_item_events(
kitchen_ticket_item_id, event_type
) values ($1,'OrderItemVoided')`,
[row.id],
);
await this.realtime.publish({
organizationId: input.organizationId,
locationId: input.locationId,
topic: 'kitchen.board',
aggregateType: 'KitchenTicket',
aggregateId: row.kitchen_ticket_id,
eventType: 'KitchenTicketChanged',
payload: { ticketId: row.kitchen_ticket_id },
});
}
return rows.length;
}
async applyRemakeCreated(input: {
organizationId: string;
locationId: string;
orderId: string;
}) {
this.assertScope(input, input.orderId, 'kitchen.order_change_invalid');
await this.realtime.publish({
organizationId: input.organizationId,
locationId: input.locationId,
topic: 'kitchen.routing',
aggregateType: 'Order',
aggregateId: input.orderId,
eventType: 'KitchenRerouteRequested',
payload: { orderId: input.orderId },
});
return { rerouteRequested: true };
}
private assertScope(input: { organizationId: string; locationId: string }, entityId: unknown, code: string): void {
if (!input || typeof input !== 'object' || !this.isBoundedText(input.organizationId, 200) ||
!this.isBoundedText(input.locationId, 200) || !this.isBoundedText(entityId, 200)) {
throw new DomainConflictError(code);
}
}
private isBoundedText(value: unknown, max: number): value is string {
return typeof value === 'string' && value.trim().length > 0 && value.length <= max;
}
}
