feat: ERPNext kill switch for referral reward payouts - #501
Open
islandbitcoin wants to merge 4 commits into
Open
feat: ERPNext kill switch for referral reward payouts#501islandbitcoin wants to merge 4 commits into
islandbitcoin wants to merge 4 commits into
Conversation
Reads the new 'Referral Settings' Single (frappe-flash-admin#75) before every payout, through a 60s cache. Polarity is the feature: money moves only on an affirmative, readable rewards_enabled=1 — off, unreadable, missing, and malformed all mean do-not-pay. Fail-closed is safe here because the gate runs BEFORE the atomic reward claim: returning leaves the invite ACCEPTED and unrewarded, a DEFERRED payout that pays on any later trigger once the switch is back on. Nothing is marked failed, nothing is lost, and an ERP outage cannot strand or double-pay a reward. Context: 2026-09-01 referral-farming wave (87 fresh accounts, 288 invites in a day, invitee->inviter chains). The deployments-level referralReward.enabled flag still exists but needs a deploy; this gives the operator a ~60s toggle in ERPNext.
- Move the ERPNext kill-switch check after the mongo invite lookups (still before the atomic claim) so non-referred KYC approvals never touch the ERP reader or log a bogus 'referral reward deferred' line; the deferral log now names only genuinely deferred rewards. Pinned by new tests asserting ERP is never consulted for non-referred / already-processed accounts. - Drop the duplicate 'unreadable kill switch' unit test; the calling contract is fully pinned by the preceding case and the error->false mapping is pinned in referral-settings.spec.ts. - Update the consumers list in src/services/frappe/coerce.ts to include app/invite/referral-settings.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEoz7nBtdtsHyuYG5wNPQV
…liation - src/services/frappe/ErpNext.ts: move getReferralSettings back below its own doc comment; it had drifted above getFeeDiscounts, leaving the Fee Discount explanation orphaned over the wrong method and getFeeDiscounts with no comment at all. - Add the retry-deferred-referral-rewards sweep promised in the original PR's ops notes but never shipped: while the ERPNext kill switch is off, KYC approvals leave their invite ACCEPTED + unrewarded (deferred, not failed). retryDeferredReferralRewards() finds that backlog and, unless --dry-run, replays awardReferralRewardOnKycApproval per distinct account — safe because that hook is itself idempotent and fail-closed. Dry-run alone also answers the review's minimum ask: an operator flipping the switch back on can see the backlog size instead of grepping logs. - New src/scripts/retry-deferred-referral-rewards.ts CLI wrapper (defaults to --dry-run) plus a yarn script entry, mirroring the existing reconcile-bridge-ibex-deposits / replay-bridge-events scripts. - Tests: retry-deferred-rewards.spec.ts covers the exact query shape, dry-run reporting, per-account dedup on replay, empty backlog, and invites with no redeemedById. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EtQzyzQ38thfC2F8BLK4u8
…reader Code-review fixes for PR #501: - retry-deferred-referral-rewards.ts defaulted --dry-run to true while its own usage doc said "drop --dry-run to actually replay it" — omitting the flag left an operator's documented command paying nobody. Flip the default to false, matching every other operator sweep script in this repo (replay-bridge-events.ts, cash-wallet-cutover.ts) and the fact that replaying is always safe (award hook is idempotent/fail-closed). Extract buildArgs() and guard execution behind require.main so the default is unit-testable against the real yargs parser. - ErpNext.getReferralSettings()'s catch block logged the error but never called recordExceptionInCurrentSpan, unlike every other query method in the class. This is the kill-switch reader consulted on every KYC approval, so losing APM visibility into its failures is a real gap during an ERP outage. Add the missing call, matching getFygaroSettings/getFeeDiscounts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EtQzyzQ38thfC2F8BLK4u8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backend half of the referral-rewards kill switch (ERP half: lnflash/frappe-flash-admin#75). Requested after the 2026-09-01 referral-farming wave: 87 same-day accounts, 288 invites, invitee→inviter chains maxing the daily limit within their first hour, ~$100 paid before detection.
Design
referralRewardsEnabledInErp()(src/app/invite/referral-settings.ts): cached (60s, failures cached too — an ERP outage degrades to deferral without a fetch storm) reader of theReferral Settingssingle. Returns true only on an affirmative, readablerewards_enabled=1; off / unreadable / missing / malformed are all "do not pay".awardReferralRewardOnKycApprovalBEFORE the atomic reward claim. Returning there leaves the invite ACCEPTED + unrewarded — a deferred payout, not a failed one. It pays on any later trigger (KYC re-fire, or an ops sweep of accepted+unrewarded invites) once the switch is re-enabled. Nothing marked failed, nothing lost, no double-pay risk.referralReward.enabledflag is unchanged and still gates first — but it needs a deploy to flip. This gives the operator a ~60s toggle in the ERPNext desk.ErpNext.getReferralSettings()+ReferralSettingsQueryErrormirror the Fygaro Settings reader exactly.Ops notes
settle-unredeemed-referrals-style sweep (or a KYC status flap) covers this; if OFF becomes long-lived, a smallretry-deferred-rewardsscript is the follow-up.make erplands the single (which defaults to ON).Tests
referral-settings.spec.ts: affirmative-only polarity (on/off/missing/malformed/read-error), fail-closed on outage, TTL memoisation of successes AND failures.award-referral-reward.spec.ts: two new cases pinning deferral semantics — switch off ⇒ no query, no claim, no payment, no failure mark.🤖 Generated with Claude Code
https://claude.ai/code/session_01NEoz7nBtdtsHyuYG5wNPQV