ADORSYS-GIS / ADORSYS-GIS/fineract-aml

Afterpay clearance — AML veto for merchant-funded BNPL (reuse graph_service + scoring_service)

Open
#3 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.