File size: 3,443 Bytes
62d663e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// ---------------------------------------------------------------------------
// settings / statementsApi.ts — the statement sender's wire (EXIT-6).
//
// ⛔ THE ONLY WRITE PATH IN THIS PRODUCT THAT LEAVES THE BUILDING. Everything
// else AIOS does to Odoo is read-only by hard block; `/admin/statements/send`
// queues real email to real customers. Three consequences for this file:
//
//   1. **No convenience wrapper sends anything.** There is no `sendAll()`, no
//      default argument that could become a send. The caller names every
//      customer explicitly, every time.
//   2. **SAFE_MODE is REPORTED here, never decided here.** The server's data
//      layer refuses an out-of-allow-list address. If this file ever grows a
//      `if (safeMode) return` it would look like the guardrail while the real
//      one silently rotted — a hidden button is a courtesy, never the check
//      ([[aios-permissioning]]).
//   3. **Outcomes are per-customer.** `sent` / `failed` / `skipped` come back as
//      lists, not counts, because "37 queued" is unverifiable and this is mail.
// ---------------------------------------------------------------------------

import { API_V1, CREDENTIALS } from "../apiContract";

export interface StatementRow {
  Customer: string;
  Email: string;
  Tier: string;
  Overdue: number;
  Receivable: number;
  [k: string]: unknown;
}

export interface StatementTemplates {
  subject: string;
  intro: string;
  footer: string;
}

export interface StatementsPayload {
  rows: StatementRow[];
  loadedAt: string;
  safeMode: boolean;
  safeRecipients: string[];
  sender: { name: string; email: string; replyTo: string; company: string };
  templates: StatementTemplates;
  tiers: string[];
}

export interface SendOutcome {
  sent: Array<{ customer: string; to: string; mailId: number }>;
  failed: Array<{ customer: string; error: string }>;
  skipped: Array<{ customer: string; reason: string }>;
  safeMode: boolean;
  test: boolean;
}

type Res<T> = { ok: true; data: T } | { ok: false; message: string };

async function call<T>(path: string, init?: RequestInit): Promise<Res<T>> {
  try {
    const r = await fetch(`${API_V1}${path}`, { credentials: CREDENTIALS, ...init });
    const body = await r.json().catch(() => null);
    if (!r.ok) {
      const msg = body?.error?.message || `request failed (${r.status})`;
      return { ok: false, message: msg };
    }
    return { ok: true, data: body as T };
  } catch (e) {
    return { ok: false, message: e instanceof Error ? e.message : "network error" };
  }
}

export function loadStatements(refresh = false) {
  return call<StatementsPayload>(`/admin/statements${refresh ? "?refresh=1" : ""}`);
}

export function previewStatement(customer: string, templates: StatementTemplates) {
  return call<{ html: string; to: string; subject: string }>("/admin/statements/preview", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ customer, templates }),
  });
}

/** Queue statements. `overrideTo` is the TEST path and takes exactly one customer. */
export function sendStatements(
  customers: string[],
  templates: StatementTemplates,
  overrideTo?: string,
) {
  return call<SendOutcome>("/admin/statements/send", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ customers, templates, overrideTo: overrideTo || undefined }),
  });
}