Skip to content

ENG-640: pay allowlisted referrals while the referral program is paused - #518

Merged
islandbitcoin merged 6 commits into
mainfrom
jabariennis/eng-640-referral-payout-allowlist
Oct 11, 2026
Merged

islandbitcoin merged 6 commits into
mainfrom
jabariennis/eng-640-referral-payout-allowlist

Conversation

@islandbitcoin

@islandbitcoin islandbitcoin commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Referral rewards are paused in prod (helm referralReward.enabled: false) since the 2026-08-31 farming wave. There is no way to pay a trusted account (an ambassador, a known referrer) without turning the whole program back on.

Change

  • New ERPNext reader ErpNext.getReferralPayoutAllowlist(): GET /api/resource/Referral%20Payout%20Allowlist with fields=["account_id","enabled"], filters=[["enabled","=",1]], limit_page_length: 0. Errors return ReferralPayoutAllowlistQueryError.
  • New src/app/invite/referral-payout-allowlist.ts: a 60s cache (successes and failures) of allowlisted account ids. It is fail-closed: an unreadable, non-array, disabled or malformed row means nobody is allowlisted. Rows are keyed by the 24-hex flash account id (frappe resolves the operator's input at save time), so the reader never touches Mongo.
  • awardReferralRewardOnKycApproval: while config.enabled is false:
    • an empty allowlist (the prod default) returns before any Mongo query, as today;
    • an allowlisted invitee proceeds as normal;
    • otherwise the inviter must be allowlisted (checked after the pending-invite lookup), or the reward is deferred with a "not allowlisted" log line.
    • Both parties are paid per the tier, and allowlisted payouts advance the global tier counter.
    • The ERPNext Referral Settings switch still gates every payout. config.enabled=true behaves exactly as before (the allowlist is not consulted).
  • Deferred-reward sweep: --account <id> / --username <name> (mutually exclusive) narrow it to one invitee, and --inviter <id> narrows it to one inviter. Both filters run inside the Mongo query. When filtered, the result reports allowlisted / inviterAllowlisted, and an empty filtered backlog warns. A malformed id or an unknown username exits 1.

No GraphQL, schema, config or mobile change. Globals.referralRewardEnabled still reads config.enabled, so the invite card stays hidden.

Operator runbook (order matters)

  1. Deploy the frappe-flash-admin Referral Payout Allowlist doctype first. If flash ships before it, the GET 404s and the allowlist reads as empty: nothing pays, and there is one error log per minute.
  2. Fund the rewards wallet before adding any row. The ERP switch is on, so a row goes live within 60s, and any KYC approval or re-fire for an allowlisted invitee (or an invitee of an allowlisted inviter) pays immediately. An underfunded payout is recorded as partial or failed and needs manual reconciliation.
  3. Add the row in ERPNext.
  4. Dry-run: node lib/scripts/retry-deferred-referral-rewards.js --configPath ... --username <name> --dry-run. Check that backlogCount > 0 and allowlisted=true.
  5. Execute the same command without --dry-run.

How it was tested

  • yarn test:unit: 279 suites, 3578 passed, 3 skipped (pre-existing), 0 failed.
  • New or extended specs:
    • referral-payout-allowlist.spec.ts (30 tests): validation, case normalisation, fail-closed on ERP error, non-array, throw and missing method, TTL memoisation, cached failures, removal after 60s.
    • award-referral-reward.spec.ts: full gate matrix. It covers config on/off × ERP switch on/off × invitee listed / inviter listed / neither × allowlist empty vs non-empty, plus already-rewarded, non-referred, missing inviterId, and tier counter cases. The existing zero-Mongo assertion is kept and strengthened for paused + empty.
    • retry-deferred-rewards.spec.ts: account / inviter / combined query filters, allowlisted reporting, empty-backlog warn, scoped replay.
    • retry-deferred-referral-rewards.spec.ts: flag parsing, --account/--username conflict, username resolution, unknown username, malformed ids.
    • ErpNext.spec.ts: URL, params, rows, non-array, no data, request error recorded in span.
  • Mutation check: disabling the inviter clause fails 3 tests, and disabling the empty-allowlist fast path fails 5.
  • yarn tsc-check, yarn tsc-check-noimplicitany, yarn eslint-check (on tracked files), prettier --check with the lockfile binary (3.6.2), typos --config typos.toml, and yarn check:sdl (no schema diff) all pass.

Follow-ups

  • The frappe-flash-admin doctype (Referral Payout Allowlist, account_id + enabled, read permission for the flash service account) must be live on each env before this deploys.
  • No per-row payout cap. Exposure is bounded by the ERP master switch and the rewards wallet balance.
  • If invitee-only scope is wanted later, delete the inviter clause in award-referral-reward.ts and drop --inviter.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6

bobodread876 and others added 4 commits October 10, 2026 19:25
GET /api/resource/Referral Payout Allowlist with fields account_id and
enabled, filtered to enabled rows, no page cap. A non-array response or a
request error returns ReferralPayoutAllowlistQueryError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
…G-640)

60s cache of allowlisted account ids (successes and failures). Unreadable,
non-array, disabled or malformed rows mean nobody is allowlisted. Rows are
keyed by the 24-hex flash account id, so no Mongo lookups.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
…ENG-640)

With referralReward.enabled=false, an empty allowlist still returns before
any Mongo query. Otherwise a referral proceeds when the invitee or the
inviter is allowlisted, and both parties are paid per the tier. The ERPNext
Referral Settings switch still gates every payout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
…(ENG-640)

--account/--username narrow to one invitee and --inviter to one inviter,
inside the Mongo query. The result reports whether the filtered party is
allowlisted, and an empty filtered backlog warns. Usage text: fund the
rewards wallet first, dry-run with --username, then execute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxkKbrg2Xohf3mQj7BLrs6
@linear

linear Bot commented Oct 11, 2026

Copy link
Copy Markdown

ENG-640

Review fix: the operator usage text now states the wallet must be funded
before the Referral Payout Allowlist row is added, and that an underfunded
payout is recorded as "partial" or "failed" (award-referral-reward.ts
writes "failed" when neither party was paid).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4SaLJF5SWq6sjBEaWJFow
`make audit` (yarn audit --level critical) fails on main since the two
Handlebars advisories published with 4.7.10 (JS injection via AST type
confusion; via own-property-check bypass). handlebars is a transitive dep
of grpc_tools_node_protoc_ts and spectaql only; the existing
`**/handlebars` resolution pinned 4.7.9. Bump it to 4.7.10. No runtime
code path uses handlebars.

Verified: `yarn audit --level critical` exit 14 (no critical) after the
bump, 30 before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4SaLJF5SWq6sjBEaWJFow
(cherry picked from commit df358ae)
@islandbitcoin
islandbitcoin merged commit 5f925ed into main Oct 11, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants