← back to AbramsOS

scripts/ingest-mode-claims.js

69 lines

#!/usr/bin/env node
// Native ingester: pull Mode Class Actions emails via the stored Gmail connector, parse them
// with lib/mode-claims, and upsert distinct settlements into settlement_claim.
// Idempotent (UNIQUE user_id+slug). $0 (Gmail API + local parse). No submits, no browser.
//
// Usage: node scripts/ingest-mode-claims.js [--days 180]
const db = require('../lib/db');
const fetcher = require('../lib/gmail-fetcher');
const { decrypt } = require('../lib/crypto');
const { parseModeEmail } = require('../lib/mode-claims');
const { id } = require('../lib/ids');

const USER = process.env.ABRAMSOS_USER_ID || 'user_steve';
const MODE_FROM = 'daniel@mail.modemobile.com';
const days = (() => { const i = process.argv.indexOf('--days'); return i > -1 ? parseInt(process.argv[i + 1], 10) : 180; })();

async function gmailRefreshToken() {
  // Prefer a Gmail connector on the steve-personal address (that's where Mode lands).
  const r = await db.query(
    `SELECT refresh_token_enc FROM connector
      WHERE user_id=$1 AND provider IN ('google','gmail') AND status='connected'
      ORDER BY updated_at DESC LIMIT 1`, [USER]);
  if (!r.rows.length) throw new Error('No connected Gmail connector — authorize one at /connectors first.');
  return decrypt(r.rows[0].refresh_token_enc);
}

async function main() {
  const token = await gmailRefreshToken();
  const q = `from:${MODE_FROM} newer_than:${days}d`;
  const ids = await fetcher.listReceiptIds(token, { query: q, max: 400 });
  console.log(`[mode-ingest] ${ids.length} messages match "${q}"`);
  let seen = 0, upserts = 0;
  for (const mid of ids) {
    const msg = await fetcher.getMessage(token, mid);
    const s = fetcher.summarize(msg);                 // { from, subject, date, text, html }
    const body = s.text || s.html || '';
    const emailDate = new Date(parseInt(msg.internalDate, 10)).toISOString();
    for (const c of parseModeEmail(body, { emailId: mid, emailDate })) {
      seen++;
      // Medical/health privacy guard (Steve 2026-08-19, TK-10723): never re-ingest medical/pharmacy
      // settlement claims — the purge must not self-undo via this 15-min scraper.
      const _MED = /\b(medical|health(care)?|hospital|surg(ery|ical)|pharmac|prescription|rx|clinic|patient|dental|mammograph|imaging|diagnos|labcorp|aetna|insurance denial)\b/i;
      if (_MED.test(String(c.category || '')) || _MED.test(String(c.slug || '')) || _MED.test(String(c.name || ''))) {
        continue; // skip medical/health claim — do not insert or upsert
      }
      const r = await db.query(
        `INSERT INTO settlement_claim
           (id,user_id,slug,name,mode_url,payout_text,payout_max_cents,deadline,proof_required,
            no_claim_required,eligibility_question,category,source_email_id,source_email_date,raw_block)
         VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
         ON CONFLICT (user_id,slug) DO UPDATE SET
           mode_url=COALESCE(EXCLUDED.mode_url,settlement_claim.mode_url),
           deadline=COALESCE(EXCLUDED.deadline,settlement_claim.deadline),
           payout_max_cents=GREATEST(COALESCE(EXCLUDED.payout_max_cents,0),COALESCE(settlement_claim.payout_max_cents,0)),
           updated_at=now()
         RETURNING (xmax=0) AS inserted`,
        [id('claim'), USER, c.slug, c.name, c.mode_url, c.payout_text, c.payout_max_cents, c.deadline,
         c.proof_required, c.no_claim_required, c.eligibility_question, c.category, c.source_email_id, c.source_email_date, c.raw_block]);
      if (r.rows[0]?.inserted) upserts++;
    }
  }
  // auto-expire past-deadline rows that were never acted on
  await db.query(`UPDATE settlement_claim SET fill_state='expired',updated_at=now()
                   WHERE user_id=$1 AND deadline < current_date AND fill_state IN ('none','queued')`, [USER]);
  console.log(`[mode-ingest] parsed ${seen} settlement blocks; ${upserts} new rows inserted.`);
  process.exit(0);
}
main().catch(e => { console.error('[mode-ingest] ERROR', e.message); process.exit(1); });