import { and, eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { interests } from '@/lib/db/schema/interests'; import { berths } from '@/lib/db/schema/berths'; import { systemSettings } from '@/lib/db/schema/system'; import { createAuditLog } from '@/lib/audit'; import { emitToRoom } from '@/lib/socket/server'; // ─── Types ──────────────────────────────────────────────────────────────────── export type BerthRuleTrigger = | 'eoi_sent' | 'eoi_signed' | 'deposit_received' | 'contract_signed' | 'interest_archived' | 'interest_completed' | 'berth_unlinked'; export type BerthRuleMode = 'auto' | 'suggest' | 'off'; export interface BerthRuleResult { action: 'applied' | 'suggested' | 'none'; newStatus?: string; message?: string; } interface RuleConfig { mode: BerthRuleMode; targetStatus: string; } interface AuditMeta { userId: string; portId: string; ipAddress: string; userAgent: string; } // ─── Defaults ──────────────────────────────────────────────────────────────── const DEFAULT_RULES: Record = { eoi_sent: { mode: 'suggest', targetStatus: 'under_offer' }, eoi_signed: { mode: 'auto', targetStatus: 'under_offer' }, deposit_received: { mode: 'auto', targetStatus: 'sold' }, contract_signed: { mode: 'auto', targetStatus: 'sold' }, interest_archived: { mode: 'suggest', targetStatus: 'available' }, interest_completed: { mode: 'auto', targetStatus: 'sold' }, berth_unlinked: { mode: 'off', targetStatus: 'available' }, }; // ─── Config ─────────────────────────────────────────────────────────────────── async function getRulesConfig( portId: string, ): Promise> { const setting = await db.query.systemSettings.findFirst({ where: and( eq(systemSettings.key, 'berth_rules'), eq(systemSettings.portId, portId), ), }); if (!setting?.value) { return { ...DEFAULT_RULES }; } const stored = setting.value as Partial>; const merged = { ...DEFAULT_RULES }; for (const trigger of Object.keys(DEFAULT_RULES) as BerthRuleTrigger[]) { if (stored[trigger]) { merged[trigger] = stored[trigger]!; } } return merged; } // ─── Evaluate Rule ──────────────────────────────────────────────────────────── export async function evaluateRule( trigger: BerthRuleTrigger, interestId: string, portId: string, meta: AuditMeta, ): Promise { const interest = await db.query.interests.findFirst({ where: and(eq(interests.id, interestId), eq(interests.portId, portId)), }); if (!interest?.berthId) { return { action: 'none' }; } const rulesConfig = await getRulesConfig(portId); const rule = rulesConfig[trigger]; if (rule.mode === 'off') { return { action: 'none' }; } if (rule.mode === 'auto') { await db .update(berths) .set({ status: rule.targetStatus, statusLastChangedBy: meta.userId, statusLastChangedReason: `Auto-applied by rule: ${trigger}`, statusLastModified: new Date(), updatedAt: new Date(), }) .where(and(eq(berths.id, interest.berthId), eq(berths.portId, portId))); void createAuditLog({ userId: meta.userId, portId, action: 'update', entityType: 'berth', entityId: interest.berthId, newValue: { status: rule.targetStatus }, metadata: { type: 'berth_rule_auto', trigger, interestId }, ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); emitToRoom(`port:${portId}`, 'berth:statusChanged', { berthId: interest.berthId, newStatus: rule.targetStatus, triggeredBy: meta.userId, trigger, }); return { action: 'applied', newStatus: rule.targetStatus }; } // suggest mode return { action: 'suggested', newStatus: rule.targetStatus, message: `Suggested status change to "${rule.targetStatus}" based on trigger "${trigger}"`, }; }