← back to AbramsOS
lib/settlement-score.js
109 lines
// lib/settlement-score.js — priority RANKING + per-item RATINGS for settlement claims.
//
// Mirrors the gated-queue-runner `scoreGate` engine (:9801 approvals viewer), but reads the
// STRUCTURED settlement_claim columns instead of parsing a memo body. For each claim it computes
// four 0-5 ratings + a composite priority + a high/med/low tier, then ranks claims priority-first.
//
// Ratings (0-5):
// value — from payout_max_cents ($ scale)
// urgency — days to deadline
// ease — auto/no-proof flat-cash is easy; proof lowers it
// winlikelihood — no-proof flat-cash = high; proof = lower
// priority = value*2.4 + urgency*2.4 + ease*1.0 + winlikelihood*0.4
// tier: high >= 26, med >= 18, else low
//
// Pure + deterministic. No DB, no network — takes a plain claim row, returns the scored fields.
// pg DATE columns come back as JS Date objects (not strings); handle both, compare calendar
// days at local midnight so 0 = due today, 1 = tomorrow, negative = expired.
function daysToDeadline(deadline) {
if (deadline == null) return null;
const dt = deadline instanceof Date ? deadline : new Date(String(deadline) + 'T00:00:00');
if (isNaN(dt.getTime())) return null;
const target = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()).getTime();
const now = new Date();
const todayMid = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
return Math.round((target - todayMid) / 86400000);
}
// A claim is "easy / high win" when no claim is required (automatic), or when the payout is a
// flat cash amount with no proof needed. Proof requirements lower both ease and win-likelihood.
function isFlatCash(payoutText) {
return /\b(no proof|flat|automatic|no claim|cash|guaranteed)\b/i.test(String(payoutText || ''));
}
function scoreClaim(claim) {
const cents = claim.payout_max_cents == null ? 0 : Number(claim.payout_max_cents);
const dollars = cents / 100;
const days = daysToDeadline(claim.deadline);
const proof = claim.proof_required === true;
const auto = claim.no_claim_required === true;
const flat = isFlatCash(claim.payout_text);
// 💰 value: dollar scale
const value =
dollars >= 10000 ? 5 :
dollars >= 5000 ? 4 :
dollars >= 1000 ? 3 :
dollars >= 100 ? 2 :
dollars > 0 ? 1 : 0;
// ⏰ urgency: days to deadline (null deadline = low urgency)
const urgency =
days == null ? 1 :
days <= 1 ? 5 :
days <= 3 ? 4 :
days <= 7 ? 3 :
days <= 30 ? 2 : 1;
// ⚡ ease: automatic / no-proof flat-cash = easiest; proof required lowers it.
let ease = 3;
if (auto) ease = 5;
else if (!proof && flat) ease = 5;
else if (!proof) ease = 4;
if (proof) ease -= 2;
ease = Math.max(0, Math.min(5, ease));
// 🎯 winlikelihood: no-proof flat-cash / automatic = high; proof-required = lower.
let win = 3;
if (auto) win = 5;
else if (!proof && flat) win = 5;
else if (!proof) win = 4;
if (proof) win -= 2;
win = Math.max(0, Math.min(5, win));
const priority = Math.round((value * 2.4 + urgency * 2.4 + ease * 1.0 + win * 0.4) * 10) / 10;
const tier = priority >= 26 ? 'high' : priority >= 18 ? 'med' : 'low';
return {
ratings: { value, urgency, ease, winlikelihood: win },
priority,
tier,
days,
dollars,
expired: days != null && days < 0,
};
}
// Score a list of claims, split into open (deadline >= today OR null) and recently-expired,
// then rank the open set by priority desc (tie-break: sooner deadline, then bigger payout).
function rankClaims(rows) {
const scored = rows.map((r) => ({ ...r, ...scoreClaim(r) }));
const open = scored.filter((s) => !s.expired);
const expired = scored.filter((s) => s.expired);
open.sort((a, b) =>
b.priority - a.priority ||
(a.days == null ? Infinity : a.days) - (b.days == null ? Infinity : b.days) ||
(Number(b.payout_max_cents) || 0) - (Number(a.payout_max_cents) || 0)
);
open.forEach((s, i) => { s.rank = i + 1; });
// expired sorted most-recently-lapsed first
expired.sort((a, b) => (b.days ?? -Infinity) - (a.days ?? -Infinity));
return { open, expired };
}
module.exports = { scoreClaim, rankClaims, daysToDeadline };