Files
pn-new-crm/src/lib/services/berth-rules-engine.ts

133 lines
4.3 KiB
TypeScript
Raw Normal View History

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, type AuditMeta } 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;
}
// ─── Defaults ────────────────────────────────────────────────────────────────
const DEFAULT_RULES: Record<BerthRuleTrigger, RuleConfig> = {
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<Record<BerthRuleTrigger, RuleConfig>> {
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<Record<BerthRuleTrigger, RuleConfig>>;
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<BerthRuleResult> {
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}"`,
};
}