import { Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { DatabaseService, type DatabaseQueryClient } from '../../../infrastructure/database/database.service';
import { DomainConflictError } from '../../../common/errors/domain-conflict.error';
import { AuditService } from '../../audit/audit.service';
import {
assertCreateAreaInput,
assertCreateTableInput,
assertLinkTablesInput,
} from './floor-input-validation';
@Injectable()
export class FloorService {
constructor(
private readonly database: DatabaseService,
private readonly audit: AuditService,
) {}
async createArea(input: {
organizationId: string;
locationId: string;
name: string;
sortOrder?: number;
actorUserId?: string;
}) {
assertCreateAreaInput(input);
const id = randomUUID();
return this.database.transaction(async (tx: DatabaseQueryClient) => {
await tx.query(
`insert into areas(id, organization_id, location_id, name, sort_order)
values ($1,$2,$3,$4,$5)`,
[id, input.organizationId, input.locationId, input.name, input.sortOrder ?? 0],
);
await this.audit.record({
organizationId: input.organizationId,
actorUserId: input.actorUserId,
eventType: 'floor.area.created',
entityType: 'area',
entityId: id,
metadata: { locationId: input.locationId },
}, tx);
return { id };
});
}
async createTable(input: {
organizationId: string;
locationId: string;
areaId: string;
tableNumber: string;
displayName?: string;
minCapacity?: number;
maxCapacity: number;
actorUserId?: string;
}) {
assertCreateTableInput(input);
const id = randomUUID();
return this.database.transaction(async (tx: DatabaseQueryClient) => {
await tx.query(
`insert into service_tables(
id, organization_id, location_id, area_id, table_number,
display_name, min_capacity, max_capacity
) values ($1,$2,$3,$4,$5,$6,$7,$8)`,
[
id,
input.organizationId,
input.locationId,
input.areaId,
input.tableNumber,
input.displayName ?? null,
input.minCapacity ?? 1,
input.maxCapacity,
],
);
await this.audit.record({
organizationId: input.organizationId,
actorUserId: input.actorUserId,
eventType: 'floor.table.created',
entityType: 'service_table',
entityId: id,
metadata: { locationId: input.locationId, areaId: input.areaId },
}, tx);
return { id };
});
}
async linkTables(input: {
organizationId: string;
locationId: string;
tableIds: string[];
actorUserId?: string;
}) {
assertLinkTablesInput(input);
if (input.tableIds.length < 2) {
throw new DomainConflictError('floor.join_requires_multiple_tables');
}
if (new Set(input.tableIds).size !== input.tableIds.length) {
throw new DomainConflictError('floor.duplicate_table_ids');
}
return this.database.transaction(async (tx: DatabaseQueryClient) => {
const rows = await tx.query<{ id: string }>(
`select id
from service_tables
where organization_id=$1
and location_id=$2
and id = any($3::uuid[])
and status='active'
for update`,
[input.organizationId, input.locationId, input.tableIds],
);
if (rows.length != input.tableIds.length) {
throw new DomainConflictError('floor.table_scope_invalid');
}
const [parent, ...children] = input.tableIds;
for (const child of children) {
await tx.query(
`insert into service_table_links(parent_table_id, child_table_id)
values ($1,$2)
on conflict do nothing`,
[parent, child],
);
}
await this.audit.record({
organizationId: input.organizationId,
actorUserId: input.actorUserId,
eventType: 'floor.tables.linked',
entityType: 'service_table',
entityId: parent,
metadata: { locationId: input.locationId, childCount: children.length },
}, tx);
return { parentTableId: parent, childTableIds: children };
});
}
}
