← back to AbramsOS
lib/commitment-extractor.js
134 lines
// Service-commitment extractor.
// Tier 1: regex over receipt body for "money-back guarantee", "30-day return policy",
// "satisfaction guaranteed", refund windows, price-match promises.
// Tier 2: local Ollama LLM fallback for ambiguous phrasings (TODO).
//
// Each extracted commitment is a structured row that pairs to a purchase + source_message.
const COMMITMENT_PATTERNS = [
{
type: 'money_back_guarantee',
re: /(?:(\d{1,3})[-\s]?day\s+)?(?:full\s+)?money[-\s]?back\s+guarantee/i,
promisedOutcome: 'refund',
confidence: 0.85,
},
{
type: 'satisfaction_guarantee',
re: /(?:100%\s+)?satisfaction\s+guarantee(?:d)?(?:\s+within\s+(\d{1,3})\s*days?)?/i,
promisedOutcome: 'refund',
confidence: 0.78,
},
{
type: 'returns_window',
re: /(?:free\s+)?returns?\s+within\s+(\d{1,3})\s*days?/i,
promisedOutcome: 'refund',
confidence: 0.80,
},
{
type: 'returns_window',
re: /(\d{1,3})[-\s]?day\s+returns?(?:\s+policy)?(?!\s+code)/i,
promisedOutcome: 'refund',
confidence: 0.78,
},
{
type: 'returns_window',
re: /return\s+within\s+(\d{1,3})\s*days?(?:\s+of\s+(?:purchase|receipt|delivery))?/i,
promisedOutcome: 'refund',
confidence: 0.80,
},
{
type: 'price_match',
re: /price[-\s]?match(?:\s+guarantee)?/i,
promisedOutcome: 'credit',
confidence: 0.72,
},
{
type: 'service_sla',
re: /(?:guaranteed\s+)?(?:next[-\s]?day|same[-\s]?day|2[-\s]?day)\s+(?:delivery|shipping|service)/i,
promisedOutcome: 'service_redo',
confidence: 0.65,
},
{
type: 'returns_window',
re: /no\s+questions?\s+asked\s+returns?(?:\s+within\s+(\d{1,3})\s*days?)?/i,
promisedOutcome: 'refund',
confidence: 0.85,
},
{
type: 'returns_window',
re: /lifetime\s+(?:return|exchange|guarantee|warranty)/i,
promisedOutcome: 'refund',
confidence: 0.85,
windowDays: 36500, // sentinel: lifetime
},
];
function extractConditions(haystack, idx, len) {
// Look ±200 chars around match for "with original receipt", "unworn", "in original packaging", etc.
const start = Math.max(0, idx - 100);
const end = Math.min(haystack.length, idx + len + 200);
const window = haystack.slice(start, end);
const conditions = [];
if (/original\s+(?:receipt|packaging|box|tags?)/i.test(window)) conditions.push('original packaging/receipt');
if (/unworn|unused|unopened/i.test(window)) conditions.push('unused');
if (/restocking\s+fee/i.test(window)) conditions.push('subject to restocking fee');
if (/excludes?\s+(?:final\s+sale|clearance)/i.test(window)) conditions.push('excludes final sale');
if (/proof\s+of\s+purchase/i.test(window)) conditions.push('proof of purchase required');
return conditions.length ? conditions.join('; ') : null;
}
/**
* Extract all service commitments from a Gmail summary.
* Returns an array of commitment objects (possibly empty).
*
* Each commitment: {
* commitment_type, guarantee_text, promised_outcome,
* window_days, conditions, confidence, source: 'heuristic',
* }
*/
function extract(summary) {
const text = (summary?.body?.text || '').slice(0, 12_000);
const html = (summary?.body?.html || '').slice(0, 12_000);
// Strip HTML tags for the haystack but keep links visible
const haystack = [
text,
html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '),
].join('\n');
const seen = new Set();
const out = [];
for (const p of COMMITMENT_PATTERNS) {
const m = haystack.match(p.re);
if (!m) continue;
const guarantee_text = m[0].trim().slice(0, 200);
const dedupeKey = `${p.type}::${guarantee_text.toLowerCase()}`;
if (seen.has(dedupeKey)) continue;
seen.add(dedupeKey);
const window_days = p.windowDays ?? (m[1] ? parseInt(m[1], 10) : null);
const conditions = extractConditions(haystack, m.index || 0, m[0].length);
out.push({
commitment_type: p.type,
guarantee_text,
promised_outcome: p.promisedOutcome,
window_days,
conditions,
confidence: p.confidence,
source: 'heuristic',
});
}
return out;
}
/**
* Convert an extracted commitment + a known purchase_date into a refund_window_ends_at.
*/
function computeWindowEnd(purchaseDate, windowDays) {
if (!purchaseDate || !windowDays) return null;
const d = new Date(purchaseDate);
if (isNaN(d.getTime())) return null;
return new Date(d.getTime() + windowDays * 86400e3);
}
module.exports = { extract, computeWindowEnd, COMMITMENT_PATTERNS };