Partner Access Scope & Revenue Share — Plan
Status: Draft — awaiting approval before Ship A kicks off.
Status (2026-06-23): shipped. Ships A–F are live in
partner_service(port 8086, DBdb_kesles_merchant_partner, schemapartner). The original design narrative below is retained; code/path references have been updated to the extracted service. Owner: Partner Platform team. Related tables:partner.partners,partner.api_credentials,partner.merchant_access,partner.merchant_consents,partner.api_audit_logs,merchant.merchants. Related docs:partner-v1-api-proposal.md,purchasing-sales-refactor-plan.md.
1. Context & Problem
1.1 Business background
Kesles Merchant works with several partner types in parallel that have very different characteristics, not a single model:
- Merchant acquisition partners (reseller, agency, affiliate, community, sales) — they look for new merchants and earn commission from the transactions of merchants they refer.
- Finance / underwriter partners (bank, lender, scoring bureau) — they need merchant transaction data for credit analysis before they extend financing.
- Ecosystem fee-share partners (QRIS operator, settlement gateway, integration partner) — they share MDR/service-fee revenue based on a contract, and their visibility may cover one ecosystem, several ecosystems, or a combination.
- Strategic stakeholders (venture capital, holding shareholders, regulators OJK/BI, internal BI / data science) — they need an aggregated view or full read-only for governance/monitoring/investment decisions.
Currently the partner API stack does not differentiate between these four categories. They all use the same authorization path, and the only visibility filter is HasMerchantAccess(partner_id, merchant_id) checked against partner.merchant_access. The effect is that whenever a new partner type with a different need shows up, engineers must add an if-else branch in the handler or a hardcoded whitelist — not scalable.
1.2 Current system state (baseline 2026-04-22)
Existing data:
merchant.partners— partner master data, thepartner_typecolumn with enumreseller | agency | affiliate | community | sales | other.bank | finance_lender | scoring_bureau | venture_capital | shareholder | regulatordo not exist yet.merchants.referral_partner_id+merchants.referral_code(migration 022) — every merchant records which partner acquired it. Already accurate for 412+ production merchants.partner.api_credentials— per-credentialallowed_scopes(jsonb) which controls OAuth-style scopes (transaction.read,scoring.read,merchant.read,risk-summary.read). The API scope (what may be read) exists, but the merchant visibility scope (which merchant may be read) does not.partner.merchant_access(migration 031) — explicit grant table per (partner_id, merchant_id, access_type). Has anaccess_typecolumn ('referral', 'consent', 'manual_grant').partner.merchant_consents— consent per merchant withconsent_scopeandconsent_expires_at. Suitable for bank/finance.partner.api_audit_logs— log of every partner request (method, path, status, scope_used). Does not yet record the visibility mode or the merchant count returned.
Existing partner endpoints (see partner-api-current-state.md):
POST /api/partner/v1/auth/tokenGET /api/partner/v1/partners/{partner_id}/referral-merchantsGET /api/partner/v1/partners/{partner_id}/referral-merchants/summaryGET /api/partner/v1/merchants/{merchant_id}/transactionsGET /api/partner/v1/merchants/{merchant_id}/scoring-summary
All of them use HasMerchantAccess or a hardcoded WHERE referral_partner_id = $self. There is no reusable visibility resolver.
1.3 Concrete pain points (per stakeholder)
Acquisition Partner team (sales/reseller):
- A sales partner asks for "my own merchants" → works. But asking "please group by referral code" → does not work because the scope is not modeled.
- A partner with 2 referral codes (e.g. online code vs offline code) → must be managed via 2 manual
partner.merchant_accessgrants. - Partner commission calculation is done outside the system (Excel by the Finance team). Each month Finance pulls the data, calculates manually, sends it to the partner. Error-prone.
Bank / Finance Partner team:
- Bank Mandiri is currently not onboarded because there is no consent-based only access mechanism — we cannot give them access to one merchant that has consented without them automatically seeing other data.
- A 3rd-party scoring bureau wants an aggregate dataset but cannot be given one because of privacy (it would see data across partners).
Fee-Share Ecosystem Partner team:
- Fee-share with Partner A only → can be done via
partner.merchant_accessbut it's clunky (must insert a grant per merchant). - Joint fee-share with Partner A + B (e.g. a settlement gateway covering 2 ecosystems) → no model. Currently worked around by creating a fake credential per ecosystem, very hard to reconcile.
- Revenue share rates differ per source (A = 3 bps, B = 5 bps) → manual in Excel.
VC / Shareholder:
- A holding shareholder owning 3 subsidiary partners A/B/C asks for a combined report of the 3 ecosystems each quarter. Currently the internal team manually exports, sends a PDF. Not real-time, not self-service.
- A VC investor in Partner A (a minority shareholder of the partner, not of Kesles) asks for a view of merchant transactions in the Partner A ecosystem. No mechanism — we cannot give it without accidentally exposing other partners' data.
Regulator / Internal:
- If OJK later requests data → it needs
globalaccess that is justified, scheduled, and auditable. Today's answer is "query the DB directly" which is not scalable for repeated questions. - The internal BI / data science team building a churn model → needs a cross-partner dataset. Currently access is via the DB replica — bypasses the privacy layer, not auditable.
1.4 Core gaps
| Gap | Consequence |
|---|---|
| No concept of visibility mode per partner | Every new need → hardcode or fake credential |
| No multi-partner visibility (combined) | Joint ecosystem fee-share cannot be modeled, multi-investment VCs cannot be served |
| No structural revenue-share rule | Commissions calculated manually in Excel, error-prone, not real-time |
| No rich partner_type taxonomy | Bank/VC/Regulator forced to be tagged 'other' → cannot be policy-distinct |
| No audit trail of access changes | If an incident occurs ("why did Partner X suddenly see merchant Y?") → no trail |
api_audit_logs does not record the effective scope | Per-partner requests are recorded, but "what mode was the partner in" is not |
| No guardrail for dangerous modes (global) | One misconfig flag can leak all data |
| No fallback policy for misconfig | Empty list → silently return all? silently return nothing? Ambiguous |
1.5 Why we cannot just "bolt on" in handlers
We considered just adding if partner_type == 'bank' then ... in each handler. Cancelled because:
- Business partner types keep growing — over the next 2 years there may be
insurance_agent,loan_marketplace,tax_consultant. Each type needs handler patches → an N×M combinatorial explosion. - Mode combinations are not linear — Partner A could simultaneously be sales (own referral), fee-share with Partner B (network), and a customer analytics buyer (global read). Cannot be answered with a single
partner_typeenum. - Compliance requires an explicit model — auditors / regulators will ask "what policy determines access?" The answer must be "a configuration table with an audit trail", not "line 247 of handlers.go".
- Self-service dashboard — the Commercial team will keep asking for "manage partner via admin UI" features that are impossible if policy is hardcoded.
So the solution must be data-driven: the visibility policy stored in a table editable via UI, with well-defined and testable semantics.
1.6 Business drivers triggering the urgency
- Q2-2026: Kesles will sign a joint fee-share contract with 2 gateway partners (A+B). Without
partner_networkmode, commission reconciliation will be done manually in Excel — Finance has refused because the transaction volume is already > 2 million/month. - Q2-2026: Bank Mandiri onboarding for the merchant financing pilot. Without a clean
explicitmode, Legal refuses to sign the partnership. - Q3-2026 (planned): New funding round → VCs will ask for portfolio monitoring access → without a controlled
partner_networkorglobal, shadow-IT will appear (separate internal dashboards and manual CSV exports). - Q3-2026 (planned): Self-service partner dashboard (partner logs in themselves, sees referral merchants, checks commission). Impossible without a consistent access model between admin-dashboard and partner-dashboard.
1.7 Viewer category + need summary
| Category | Examples | Needs access | Request frequency | Volume |
|---|---|---|---|---|
| Own referral | Sales / reseller / agency / affiliate partners | Only merchants they referred | Daily dashboard | Hundreds |
| Explicit consent | Bank, finance lender, scoring bureau | Only merchants with written consent | On-demand during underwriting | Tens |
| Partner network | VC investor in Partner A, holding shareholder A+B+C, joint ecosystem fee-share | Combined from a chosen set of partners | Monthly dashboard + weekly commission basis | Thousands |
| Global | Kesles VC shareholder, OJK regulator, internal BI | All merchants | Rare, scheduled | Millions |
Beyond visibility, fee-sharing partners need commission rules that may differ per (beneficiary, source_ecosystem, fee_type). These rules are separate from visibility — because one beneficiary may see many sources but only earn commission from a subset.
1.8 Non-goals
What is not the goal of this document (handled elsewhere):
- Partner authentication (OAuth client_credentials) — already OK in
partner-v1-api-proposal.md. - Technical rate limiting — deferred to the infrastructure layer.
- Billing engine (actual invoice generation + payout) — this document just provides the commission basis data, the billing engine is separate.
- Partner self-service dashboard (partner logs into their own dashboard) — separate roadmap (Q3+).
- Per-field data masking / PII redaction — already handled in the masking output layer, not touched.
2. Scope In / Out
In:
- Access scope model (mode + partner list) on
partner.partnersand override onpartner.api_credentials. - Backend resolver helper (
ResolveMerchantScope). - Refactor existing
/api/partner/v1/...endpoints to use the helper. - Audit trail on mode changes + per-request audit log.
- Dashboard Partner edit UI (mode dropdown + multi-select network).
- Revenue-share rules (billing logic separated from visibility).
- Super_admin guardrail for
globalmode.
Out (deferred):
partner.access_scope.changedwebhook (Ship F, optional).- Email notification to the beneficiary partner (Ship F).
- Self-service portal where partners set their own scope (v2).
- Time-boxed access (consent expiry) — already exists as
consent_expires_atinpartner.merchant_consents, not touched.
3. Terminology
| Term | Definition |
|---|---|
| Viewer partner | The partner who uses the API — has a credential, calls /api/partner/v1/.... The asker. |
| Source partner | The partner who acquired a merchant — merchants.referral_partner_id. The owner of merchants in the ecosystem. |
| Beneficiary partner | The partner who receives a revenue share from the source merchant's transactions. May equal the viewer. |
| Access scope mode | Enum own_referral | explicit | partner_network | global. Determines the visible-merchant filter. |
| Access partner IDs | An array of UUIDs. Only relevant when mode = partner_network. The list of source partners that may be seen. |
Viewer can = source can = beneficiary (a regular sales partner). They can also all differ (VC viewer, Partner A as source, Partner A also as beneficiary).
4. Access Scope Semantics
4.1 Mode table
| Mode | WHERE fragment | Use case |
|---|---|---|
own_referral (default) | m.referral_partner_id = $self_id OR EXISTS partner.merchant_access | Sales / reseller — see own merchants |
explicit | EXISTS partner.merchant_access only | Bank/lender — only via consent |
partner_network | m.referral_partner_id = ANY($access_partner_ids) | VC, shareholder, joint ecosystem fee-share |
global | TRUE (no filter) | Regulator, Kesles VC, internal BI |
Fail-closed when partner_network but the list is empty → WHERE FALSE (cannot see anything). Prevents misconfig from becoming an open door.
Implementation note: the scope-filter logic lives in
services/partner_service/internal/partner/scope_resolver.go. Theown_referralmode resolvesm.referral_partner_id = $self_idunioned with explicit grants inpartner.merchant_access, so merchants reached via a manual grant (not viareferral_partner_id) are also counted in commission-basis and portfolio results.