import { DomainConflictError } from '../../../common/errors/domain-conflict.error';
type InventoryInput = Record<string, unknown>;
function assertObject(input: unknown, code: string): asserts input is InventoryInput {
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new DomainConflictError(code);
}
function assertText(value: unknown, max: number, code: string): asserts value is string {
if (typeof value !== 'string' || value.trim().length === 0 || value.length > max) throw new DomainConflictError(code);
}
function assertOptionalText(value: unknown, max: number, code: string): void {
if (value !== undefined && value !== null) assertText(value, max, code);
}
function assertScope(input: InventoryInput, code: string): void {
assertText(input.organizationId, 200, code);
assertText(input.locationId, 200, code);
}
function assertDecimal(value: unknown, code: string, options: { positive?: boolean; nonZero?: boolean; nonNegative?: boolean } = {}): void {
if (typeof value !== 'string' || value.length > 64 || !/^-?\d+(?:\.\d+)?$/.test(value)) {
throw new DomainConflictError(code);
}
const numeric = Number(value);
if (!Number.isFinite(numeric) || (options.positive && numeric <= 0) || (options.nonZero && numeric === 0) || (options.nonNegative && numeric < 0)) {
throw new DomainConflictError(code);
}
}
function assertDate(value: unknown, code: string): void {
assertText(value, 10, code);
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new DomainConflictError(code);
const parsed = new Date(`${value}T00:00:00.000Z`);
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) throw new DomainConflictError(code);
}
function assertJsonRecord(value: unknown, code: string, maxKeys = 100): void {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new DomainConflictError(code);
if (Object.keys(value as object).length > maxKeys) throw new DomainConflictError(code);
try {
if (JSON.stringify(value).length > 100_000) throw new DomainConflictError(code);
} catch (error) {
if (error instanceof DomainConflictError) throw error;
throw new DomainConflictError(code);
}
}
export function assertStockMovementInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.input_invalid';
assertObject(input, code);
assertScope(input, code);
for (const key of ['stockItemId', 'storageLocationId', 'unitCode', 'normalizedUnitCode']) assertText(input[key], 64, code);
if (!['purchase_receipt', 'sale_consumption', 'production', 'transfer_in', 'transfer_out', 'waste', 'return', 'manual_adjustment', 'inventory_count_adjustment', 'breakage', 'staff_meal', 'trust_consumption'].includes(String(input.movementType))) throw new DomainConflictError(code);
assertDecimal(input.quantityDelta, code, { nonZero: true });
assertDecimal(input.normalizedQuantityDelta, code, { nonZero: true });
assertOptionalText(input.sourceType, 120, code);
assertOptionalText(input.sourceId, 200, code);
assertOptionalText(input.reason, 1000, code);
assertOptionalText(input.employeeId, 200, code);
assertDate(input.businessDate, code);
if (input.consumptionGeneration !== undefined && (!Number.isSafeInteger(input.consumptionGeneration) || Number(input.consumptionGeneration) < 1 || Number(input.consumptionGeneration) > 1000)) throw new DomainConflictError(code);
}
export function assertManualAdjustmentInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.adjustment_input_invalid';
assertObject(input, code);
assertStockMovementInput({ ...input, movementType: 'manual_adjustment' });
assertText(input.reason, 1000, code);
}
export function assertWasteInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.waste_input_invalid';
assertObject(input, code);
assertScope(input, code);
for (const key of ['stockItemId', 'storageLocationId', 'unitCode', 'normalizedUnitCode']) assertText(input[key], 64, code);
assertDecimal(input.quantity, code, { positive: true });
assertDecimal(input.normalizedQuantity, code, { positive: true });
assertText(input.reason, 200, code);
assertDate(input.businessDate, code);
for (const key of ['notes', 'sourceType', 'sourceId', 'employeeId']) assertOptionalText(input[key], key === 'notes' ? 4000 : 200, code);
}
export function assertTransferInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.transfer_input_invalid';
assertObject(input, code);
assertScope(input, code);
for (const key of ['stockItemId', 'fromStorageLocationId', 'toStorageLocationId', 'unitCode', 'normalizedUnitCode']) assertText(input[key], 64, code);
if (input.fromStorageLocationId === input.toStorageLocationId) throw new DomainConflictError('inventory.transfer_same_location');
assertDecimal(input.quantity, code, { positive: true });
assertDecimal(input.normalizedQuantity, code, { positive: true });
assertDate(input.businessDate, code);
assertOptionalText(input.reason, 1000, code);
assertOptionalText(input.employeeId, 200, code);
}
export function assertCountCreateInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.count_input_invalid';
assertObject(input, code);
assertScope(input, code);
assertOptionalText(input.storageLocationId, 200, code);
if (input.scope !== undefined && input.scope !== null) assertJsonRecord(input.scope, code);
}
export function assertCountReferenceInput(organizationId: unknown, locationId: unknown, countId: unknown): void {
const code = 'inventory.count_input_invalid';
assertText(organizationId, 200, code);
assertText(locationId, 200, code);
assertText(countId, 200, code);
}
export function assertCountStartInput(input: unknown): asserts input is InventoryInput {
assertObject(input, 'inventory.count_input_invalid');
assertCountReferenceInput(input.organizationId, input.locationId, input.countId);
assertOptionalText(input.employeeId, 200, 'inventory.count_input_invalid');
}
export function assertCountLineInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.count_input_invalid';
assertObject(input, code);
assertCountReferenceInput(input.organizationId, input.locationId, input.countId);
assertText(input.lineId, 200, code);
assertDecimal(input.countedQuantity, code, { nonNegative: true });
assertDecimal(input.normalizedCountedQuantity, code, { nonNegative: true });
assertOptionalText(input.notes, 4000, code);
}
export function assertCountPostInput(input: unknown): asserts input is InventoryInput {
assertObject(input, 'inventory.count_input_invalid');
assertCountReferenceInput(input.organizationId, input.locationId, input.countId);
assertDate(input.businessDate, 'inventory.count_input_invalid');
assertOptionalText(input.employeeId, 200, 'inventory.count_input_invalid');
}
export function assertStorageLocationCreateInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.setup_input_invalid';
assertObject(input, code);
assertScope(input, code);
assertText(input.name, 250, code);
assertText(input.storageType, 80, code);
if (input.sortOrder !== undefined && (!Number.isSafeInteger(input.sortOrder) || Number(input.sortOrder) < 0 || Number(input.sortOrder) > 100_000)) throw new DomainConflictError(code);
}
export function assertStockItemCreateInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.setup_input_invalid';
assertObject(input, code);
assertText(input.organizationId, 200, code);
assertOptionalText(input.ingredientId, 200, code);
assertText(input.name, 250, code);
assertText(input.baseUnitCode, 64, code);
}
export function assertLocationConfigurationInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.setup_input_invalid';
assertObject(input, code);
assertText(input.stockItemId, 200, code);
assertText(input.locationId, 200, code);
assertText(input.defaultStorageLocationId, 200, code);
if (input.isTracked !== undefined && typeof input.isTracked !== 'boolean') throw new DomainConflictError(code);
}
export function assertReorderRuleInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.setup_input_invalid';
assertObject(input, code);
assertScope(input, code);
assertText(input.stockItemId, 200, code);
assertDecimal(input.reorderPoint, code, { positive: true });
if (input.targetStock !== undefined && input.targetStock !== null) assertDecimal(input.targetStock, code, { positive: true });
assertText(input.unitCode, 64, code);
}
export function assertTrustBudgetConfigureInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.trust_budget_input_invalid';
assertObject(input, code);
assertScope(input, code);
if (typeof input.enabled !== 'boolean') throw new DomainConflictError(code);
if (!['day', 'week', 'month'].includes(String(input.period))) throw new DomainConflictError(code);
if (!Number.isSafeInteger(input.budgetMinor) || Number(input.budgetMinor) < 0) throw new DomainConflictError(code);
if (typeof input.currencyCode !== 'string' || !/^[A-Z]{3}$/.test(input.currencyCode)) throw new DomainConflictError(code);
if (!['notify_only', 'require_reason', 'manager_approval'].includes(String(input.overLimitBehavior))) throw new DomainConflictError(code);
if (!Array.isArray(input.allowedReasonKeys) || input.allowedReasonKeys.length < 1 || input.allowedReasonKeys.length > 20) throw new DomainConflictError(code);
const unique = new Set<string>();
for (const reason of input.allowedReasonKeys) {
assertText(reason, 64, code);
if (unique.has(reason)) throw new DomainConflictError(code);
unique.add(reason);
}
assertOptionalText(input.userId, 200, code);
}
export function assertTrustBudgetRecordInput(input: unknown): asserts input is InventoryInput {
const code = 'inventory.trust_budget_input_invalid';
assertObject(input, code);
assertScope(input, code);
for (const key of ['stockItemId', 'storageLocationId', 'unitCode', 'normalizedUnitCode']) assertText(input[key], 64, code);
assertDecimal(input.quantity, code, { positive: true });
assertDecimal(input.normalizedQuantity, code, { positive: true });
assertDate(input.businessDate, code);
assertText(input.reasonKey, 64, code);
assertOptionalText(input.detailNote, 1000, code);
assertOptionalText(input.employeeId, 200, code);
}
