ADORSYS-GIS / ADORSYS-GIS/fineract-aml
Afterpay clearance — AML veto for merchant-funded BNPL (reuse graph_service + scoring_service)
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
> **Status:** Design/spec ticket. This is the **AML-side dependency** of the afterpay (merchant-funded BNPL) design in [webank-mobile#103](https://github.com/ADORSYS-GIS/webank-mobile/issues/103). It scopes only what AML must expose; the product flow lives in #103.
## Summary
Afterpay lets a customer split an in-app merchant payment into installments while the **merchant is paid in full upfront** — Webank fronts the cash over the Fineract credit rail. Because real money leaves before the customer repays, **collusion** (fake merchant + fake buyer to extract the upfront cash) is the dominant fraud. Per the agreed module boundaries (#103 §8), **AML is the risk authority / veto**; the commercial "who is a good merchant to invite" scorecard stays out of AML.
This ticket adds an **afterpay clearance** capability that reuses existing AML services — it does **not** build new risk infrastructure.
## What AML must expose
A **clearance verdict** at two gating points:
| Gate | When | Latency |
|------|------|---------|
| **(a) Entity clearance** — is this merchant safe to *invite* to afterpay? | Periodic / batch, before the app shows the offer | Async OK |
| **(b) Transaction clearance** — is *this* afterpay disbursement safe? (merchant + buyer + amount) | Real-time, at checkout, **before fronting cash** | Synchronous, low-latency |
Gate (b) is where collusion crystallises (the cash-out moment), so it must be in — or pre-cached for — the synchronous checkout path.
### Proposed endpoints (mirror the existing `api/credit.py` pattern)
```
POST /afterpay/clearance/merchant # entity-level (gate a)
body: { user_id }
returns: ClearanceVerdict
POST /afterpay/clearance/transaction # per-disbursement (gate b)
body: { merchant_user_id, buyer_user_id, amount, currency }
returns: ClearanceVerdict
```
### `ClearanceVerdict` schema (new `schemas/afterpay.py`)
```
{
"decision": "clear" | "hold" | "deny",
"risk_score": float,
"reasons": [string], // human-readable, audit-safe
"linkage_findings": [ // from graph_service
{ "type": "shared_device|shared_sim|cni_family|recovery|referral|funding|cycle",
"from": user_id, "to": user_id, "confidence": float }
],
"expires_at": timestamp // entity verdicts are cacheable until expiry
}
```
## Reuse — what this builds on (no new engines)
| Need | Existing AML service | Use |
|------|----------------------|-----|
| Collusion cycles / clusters; **buyer ⇄ merchant linkage** (device/SIM/CNI/recovery/referral/funding edges) | `graph_service.py` (`api/graph.py`, `schemas/graph.py`) | Counterparty-independence check + closed-loop detection |
| Entity & transaction **risk score**, velocity / structuring anomaly | `scoring_service.py` (`api/scoring.py`) | Drives `risk_score` + `decision` |
| Transaction history / monitoring | `transaction_service.py` | Inputs for scoring & velocity |
| Sanctions / adverse media | `sanctions_service.py`, `adverse_media_service.py` | Hard-deny conditions |
| **Hold/deny follow-up** | `alert_service.py`, `case_service.py` | A `hold`/`deny` raises an alert / opens a case |
| **Audit trail** | `audit_service.py` | Every verdict is logged, regulator-facing |
| Precedent | `credit_service.py` / `api/credit.py` | Afterpay *is* merchant-funded credit — same clearance shape |
New code is thin: `api/afterpay.py` + `services/afterpay_service.py` + `schemas/afterpay.py` that **orchestrate** the above into a verdict.
## Boundary (mandate purity — #103 §8)
- AML returns **risk only**. It must **not** compute the merchant-quality / commercial scorecard (fan-in breadth, counterparty entropy, conversion fit) — that lives in webank-mobile's behaviour-profile layer. Mixing growth-targeting into a module that files SARs/CTRs would pressure risk thresholds toward conversion.
- The caller (BFF) composes: `invite = commercial_score ≥ T AND AML.clearance(entity)==clear`; `disburse = AML.clearance(txn)==clear AND credit_eligible AND within caps/reserve`.
## Collusion signals AML should weigh (gate b)
- Closed-loop / cyclic flow among a tight cluster (`graph_service`).
- Merchant's "customers" are also its **funders**.
- Buyer ⇄ merchant share **device / SIM / CNI-family / recovery contacts / referral chain / funding source** → `counterparty_independent = false` → `hold`/`deny`.
- Buyer or merchant under an open AML case/alert.
- Velocity / behaviour cliff right after a merchant's afterpay opt-in.
## Non-functional
- **Latency:** gate (b) target p95 well under the checkout budget; pre-cache entity-level risk, compute only the incremental buyer⇄merchant linkage + amount check inline.
- **Idempotency & audit:** every verdict persisted via `audit_service` with inputs + reasons.
- **Fail-closed:** if AML is unavailable at gate (b), default to **deny/hold** (do not front cash uncleared).
- **Verdict caching:** entity verdicts cacheable until `expires_at`; transaction verdicts are single-use.
## Open questions
1. Push model vs. pull — should AML also emit a `webhook` (`api/webhook.py`) when a previously-cleared merchant becomes high-risk, so BFF can revoke afterpay proactively?
2. Risk-score thresholds for `clear` / `hold` / `deny` — reuse the credit risk bands or define afterpay-specific bands?
3. Does `hold` auto-open a case, or only alert until an operator triages?
4. Linkage-edge sources available today in `graph_service` (which of device/SIM/CNI/recovery/referral/funding are already modelled vs. to be added)?
## Phasing
- **MVP:** synchronous `POST /afterpay/clearance/transaction` (gate b) reusing `graph_service` + `scoring_service`; fail-closed; audit; `hold`/`deny` raises an alert. Entity clearance (gate a) can start as a batch query over the same logic.
- **V2:** webhook-driven proactive revocation; afterpay-specific scoring band; case auto-open with `escalation_service`.
## Links
- Product design & flow: [webank-mobile#103](https://github.com/ADORSYS-GIS/webank-mobile/issues/103)
- KYC model & behaviour profile: webank-mobile#87
- COBAC-safe fee framing: webank-mobile#95
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading api/credit.py and the referenced graph_service.py, scoring_service.py, transaction_service.py, alert_service.py, case_service.py, and audit_service.py to understand the existing clearance patterns. The proposed MVP is organized into api/afterpay.py, services/afterpay_service.py, and schemas/afterpay.py; done means the transaction clearance path reuses existing services, fails closed, audits verdicts, and raises alerts for hold or deny outcomes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100