dodil/erp-brick-payments
DODIL ERP brick: payments
src/brick_payments/infrastructure/ai_steps.py
"""AI steps (the AI-step contract): the mode is part of the process. All three of this brick's steps only SUGGEST;
`ai_suggest_match` may be opted (by pay:settings:controls) into auto-with-review for EXACT matches.
ai_suggest_match baseline (domain/matching.py): reference text and sums, no model call - always explainable, and
useful with no provider wired. The suggestion row carries the evidence, a confidence and the model.
ai_draft_reminder the level's own template, filled in (the spec's fallback); a provider may redraft it later.
late_payment_risk a ranking on the overdue list: how late, how often - the customer's own payment history.
Tests never call a model."""
from datetime import date
from decimal import Decimal
AI_STEPS = {
"ai_suggest_match": {"mode": "suggest", "modes_allowed": ["off", "suggest", "auto-with-review"], "safe_for_auto": False,
"fallback": "the receipt stays unallocated for the cashier"},
"ai_draft_reminder": {"mode": "suggest", "modes_allowed": ["off", "suggest", "auto"], "safe_for_auto": False,
"fallback": "the level's template, filled in"},
"late_payment_risk": {"mode": "suggest", "modes_allowed": ["off", "suggest"], "safe_for_auto": False,
"fallback": "the list sorts by age and amount"},
}
def draft_reminder(template: str, *, customer: str, invoice_number: str, outstanding: Decimal, currency: str,
due_date: date, level: int) -> str:
"""The level's template, filled in. {customer} {invoice} {amount} {currency} {due_date} {level}; an empty template
gets a plain sentence that names the invoice and the amount."""
body = template or ("Our records show invoice {invoice} for {amount} {currency}, due on {due_date}, is still unpaid. "
"Please arrange payment, or tell us if it has crossed with your payment.")
return body.format(customer=customer, invoice=invoice_number, amount=f"{outstanding:,.2f}", currency=currency,
due_date=due_date.isoformat(), level=level)
def risk(overdue_days: int, late_paid: int, paid: int) -> dict:
"""A transparent late-payment risk: the share of this customer's paid invoices that were paid late, and how late the
open one already is. A ranking for the collector, never a decision."""
share = Decimal(late_paid) / Decimal(paid) if paid else Decimal(0)
score = min(Decimal(1), share * Decimal("0.6") + Decimal(min(overdue_days, 90)) / Decimal(90) * Decimal("0.4"))
return {"score": str(score.quantize(Decimal("0.01"))),
"reasons": [f"{overdue_days} days past due", f"{late_paid} of {paid} earlier invoices paid late" if paid else "no payment history yet"]}