1
function isRecord(value: unknown): value is Record<string, any> {
2
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
}
4
5
function text(value: unknown, max: number, code: string): asserts value is string {
6
if (typeof value !== 'string' || value.trim().length === 0 || value.length > max) throw new Error(code);
7
}
8
9
function optionalText(value: unknown, max: number, code: string): void {
10
if (value !== undefined && value !== null) text(value, max, code);
11
}
12
13
function boundedStringList(value: unknown, max: number, code: string): void {
14
if (value === undefined || value === null) return;
15
if (!Array.isArray(value) || value.length > max || value.some((item) => typeof item !== 'string' || item.trim().length === 0 || item.length > 200)) {
16
throw new Error(code);
17
}
18
}
19
20
export function assertPmsContextInput(input: unknown): asserts input is Record<string, any> {
21
if (!isRecord(input)) throw new Error('pms_context.input_invalid');
22
if (input.source !== 'pms' || typeof input.stayId !== 'string' || !input.stayId.trim() || typeof input.hotelId !== 'string' || !input.hotelId.trim()) {
23
throw new Error('pms_context.identity_invalid');
24
}
25
text(input.stayId, 200, 'pms_context.input_invalid');
26
text(input.hotelId, 200, 'pms_context.input_invalid');
27
optionalText(input.roomNumber, 32, 'pms_context.input_invalid');
28
if (!['in_house', 'arriving', 'departed'].includes(input.status)) throw new Error('pms_context.state_invalid');
29
if (input.availability !== undefined && !['available', 'degraded', 'unavailable'].includes(input.availability)) throw new Error('pms_context.availability_invalid');
30
}
31
32
export function assertHotelFnbCompositionInput(input: unknown): asserts input is Record<string, any> {
33
if (!isRecord(input) || !isRecord(input.pms) || !isRecord(input.guest) || !isRecord(input.outlet)) {
34
throw new Error('hotel_fnb_context.input_invalid');
35
}
36
}
37
38
export function assertHotelGuestContextInput(input: unknown): asserts input is Record<string, any> {
39
if (!isRecord(input)) throw new Error('hotel_guest_context.input_invalid');
40
if (typeof input.stayId !== 'string' || !input.stayId.trim()) throw new Error('hotel_guest_context.stay_invalid');
41
text(input.stayId, 200, 'hotel_guest_context.input_invalid');
42
optionalText(input.roomNumber, 32, 'hotel_guest_context.input_invalid');
43
optionalText(input.entitlement, 200, 'hotel_guest_context.input_invalid');
44
optionalText(input.preference, 500, 'hotel_guest_context.input_invalid');
45
if (typeof input.privacyAllowed !== 'boolean') throw new Error('hotel_guest_context.privacy_invalid');
46
if (input.roomChargeAllowed !== undefined && typeof input.roomChargeAllowed !== 'boolean') throw new Error('hotel_guest_context.input_invalid');
47
}
48
49
export function assertOutletNetworkInput(input: unknown): asserts input is Record<string, any> {
50
if (!isRecord(input)) throw new Error('multi_outlet.input_invalid');
51
if (typeof input.groupId !== 'string' || !input.groupId.trim() || typeof input.outletId !== 'string' || !input.outletId.trim()) throw new Error('multi_outlet.identity_invalid');
52
text(input.groupId, 200, 'multi_outlet.input_invalid');
53
text(input.outletId, 200, 'multi_outlet.input_invalid');
54
optionalText(input.guestId, 200, 'multi_outlet.input_invalid');
55
optionalText(input.transferToOutletId, 200, 'multi_outlet.input_invalid');
56
optionalText(input.reportMetric, 200, 'multi_outlet.input_invalid');
57
boundedStringList(input.sharedResourceIds, 100, 'multi_outlet.input_invalid');
58
if (input.transferToOutletId?.trim() === input.outletId.trim()) throw new Error('multi_outlet.identity_invalid');
59
}
60
61
export function assertRoomserviceInput(input: unknown): asserts input is Record<string, any> {
62
if (!isRecord(input)) throw new Error('roomservice.input_invalid');
63
if (typeof input.orderId !== 'string' || !input.orderId.trim() || typeof input.roomNumber !== 'string' || !input.roomNumber.trim()) throw new Error('roomservice.identity_invalid');
64
text(input.orderId, 200, 'roomservice.input_invalid');
65
text(input.roomNumber, 32, 'roomservice.input_invalid');
66
if (typeof input.productionReady !== 'boolean' || typeof input.deliveryTaskReady !== 'boolean') throw new Error('roomservice.input_invalid');
67
boundedStringList(input.resourceIds, 100, 'roomservice.input_invalid');
68
}
69
70
const settlementSources = ['payment', 'room_folio', 'package', 'allowance', 'house_account'] as const;
71
72
export function assertSettlementInput(input: unknown): asserts input is Record<string, any> {
73
if (!isRecord(input)) throw new Error('settlement.input_invalid');
74
if (!Number.isSafeInteger(input.totalCents) || input.totalCents <= 0 || !Array.isArray(input.allocations) || input.allocations.length === 0 || input.allocations.length > 32) throw new Error('settlement.invalid_total');
75
for (const allocation of input.allocations) {
76
if (!isRecord(allocation) || !settlementSources.includes(allocation.source) || !Number.isSafeInteger(allocation.amountCents) || allocation.amountCents <= 0) throw new Error('settlement.invalid_allocation');
77
optionalText(allocation.reference, 500, 'settlement.invalid_allocation');
78
}
79
}
80