feat(fygaro): fee-aware auto-credit — credit net, configurable via ERPNext - #473
Merged
Conversation
…PNext The Fygaro card-topup webhook credited the FULL gross face value, but Fygaro/PayPal take a processor cut off what settles to Flash and Flash wants a margin — so every top-up lost money. This makes the credited amount the NET, driven by operator-configurable fees. Fee source — ERPNext "Fygaro Settings" (Single doctype), read via a new ErpNext.getFygaroSettings() and memoised ~60s (fygaro-settings.ts) so the webhook never hammers ERPNext per payment. A failed/garbage read resolves to "unavailable" (never "assume zero fees"). Fee formula (integer cents; each fee rounded to the nearest cent): gross_cents = round(amount * 100) processor_fee_cents = round(gross * processor_fee_percent/100) + round(processor_fee_fixed * 100) flash_fee_cents = round(gross * flash_margin_percent/100) + round(flash_margin_fixed * 100) net_cents = gross - processor_fee - flash_fee e.g. $10.00 → processor $0.79, flash $0.20, net $9.01. Gating — auto-credit runs ONLY IF all hold: (1) yaml credit.enabled master gate, (2) settings available AND auto_credit_enabled, (3) currency USD, (4) gross ≤ auto_credit_limit, (5) net > 0. Otherwise RECORD-ONLY: row stays Fiat Received, no credit, and one ops alert (notifyOpsEvent + alertBridge) names the failing gate (settings-unavailable / auto-credit- disabled / non-usd / over-limit / non-positive-net). The deploy-level credit-disabled gate records silently, as before. On credit: send net_cents (not gross) and stamp the ERPNext row with initial_amount (gross), processor_fee, flash_fee, final_amount (net). All existing safety is preserved — idempotency on the tx id, Pending-means- credited, the releasing-lock + Completed processed-marker, and the ResourceAttemptsLockServiceError-vs-other-error split. globals.fygaroTopup — new nullable public field exposing minimumAmount, processorFeePercent, processorFeeFixed, flashFeePercent, flashFeeFixed from the cached settings, so the app can preview "you'll receive $X" locally; null when settings are unavailable (app degrades). SDL regenerated. Tests: fee-math table + rounding, full gating matrix, settings caching / failure fallback, and populated row fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb
…tings retry, resolver test
Code-review fixes for the fee-aware auto-credit path:
- fees.ts: enforce minimum_topup server-side. It was read/validated/cached and
exposed via GraphQL but never gated crediting, so a $2 top-up auto-credited
under a $10 minimum. Add an inclusive `under-minimum` gate (checked after the
net gate so a non-positive-net tiny payment still reports the more fundamental
reason). Symmetric with the existing inclusive auto_credit_limit upper bound.
- payment.ts: reject a non-numeric amount ("abc") with 400 up front. Previously
Math.round(Number("abc")*100) was NaN, slipped past every numeric gate into
the credit path, and surfaced as a CRITICAL "auto-credit failed" page —
misclassifying garbage input as a credit failure.
- payment.ts: return 500 for the transient `settings-unavailable` reason instead
of acking 200. A brief ERPNext blip (settings cached undefined up to 60s) was
permanently downgrading a payment to manual credit because 200 stops Fygaro
retrying. Now Fygaro retries and the read self-heals; no dedupe lock is taken
(it would block the retry) and the un-dedupable ops-feed line is skipped, while
the TTL-deduped alert still pages a sustained outage. Deterministic reasons
keep their 200 record-only behavior.
- Tests: add a Globals `fygaroTopup` resolver spec (null when settings absent;
each field pinned to its exact source with all-distinct values to catch a
field swap); add fees + payment coverage for `under-minimum`; add the 400
non-numeric-amount case; move `settings-unavailable` to its own 500 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QwZjNcKfkMhrBE33HcUVN
Address the residual review note: document that the credit path always writes explicit fee strings (incl. "0.00" for a zero-rate promo) via centsToDollars, so the admin's "Pending" display only ever appears on uncredited rows. No behavior change — centsToDollars already never returns undefined; this pins the contract against a future refactor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb
bobodread876
approved these changes
Aug 11, 2026
islandbitcoin
added a commit
to lnflash/flash-mobile
that referenced
this pull request
Aug 11, 2026
…d top-ups (#688) * feat(topup): show net "you'll receive" and enforce $10 minimum on card top-ups Fast-follow to the merged backend fee feature (lnflash/flash#473), which exposes Globals.fygaroTopup with the processor/Flash fee params and the top-up minimum. - Add `fygaroTopup { minimumAmount processorFeePercent processorFeeFixed flashFeePercent flashFeeFixed }` to the `transferFlags` globals query that the topup/cashout entry screen already fetches, so the data is cache-warm on arrival. The field + the `FygaroTopupInfo` type were hand-added to public-schema.graphql (no backend access to re-introspect) and codegen was regenerated. - TopupDetails now shows "You'll receive $X.XX" (net after fees) live as the user types, mirroring the cashout settlement-amount preview (#683). Card-only; the line is hidden when `fygaroTopup` is null so a wrong number is never shown. - Raise/enforce the card minimum to `fygaroTopup.minimumAmount` (fallback $10 when null); the invalid-amount alert now states the real minimum. - New i18n keys added to en + all 23 locale files. Tests cover the $10 floor, the $5 rejection (incl. null fallback), and the $9.01 net for a $10 gross. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb * fix(topup): mirror backend cent-rounding in net estimate, gate preview on minimum Address code-review findings on the "you'll receive" top-up disclosure: - estimateTopupNet now computes in integer cents, rounding each fee component (processor %+fixed, flash %+fixed) to the nearest cent before subtracting, exactly as the backend does (flash#473). The prior float-dollar math with a single final round diverged by a cent for a large fraction of non-round amounts (e.g. $10.25 showed $9.25 while the backend credits $9.24), over-promising the headline number. - The net preview is now gated on the enforced minimum, so a below-floor amount (e.g. $5) no longer shows a concrete receive figure that Continue will immediately refuse. - Tests: added a non-round unit case (10.25 → $9.24) and a preview case (10.25 → $9.24, not $9.25) that fail under the old float math, plus a below-minimum hide case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QwZjNcKfkMhrBE33HcUVN --------- Co-authored-by: Dread <dread@example.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Money flow
The Fygaro card-topup webhook credited the full gross face value. But Fygaro/PayPal take a processor cut off what actually settles to Flash, and Flash wants a margin — so today Flash loses money on every top-up. This PR makes the credited amount the NET, driven by operator-configurable fees, and stamps the fee breakdown onto the ERPNext audit row.
Fee source — ERPNext "Fygaro Settings"
A new
ErpNext.getFygaroSettings()reads theFygaro SettingsSingle doctype (processor,processor_fee_percent,processor_fee_fixed,flash_margin_percent,flash_margin_fixed,auto_credit_limit,minimum_topup,auto_credit_enabled). It is memoised for ~60s infygaro-settings.tsso the webhook never hammers ERPNext per payment. A failed read, a missing row, or a malformed row all resolve to unavailable — never "assume zero fees" — and the failure is cached so an outage degrades to record-only without a fetch storm.The formula (integer cents; each fee rounded to the nearest cent)
$10.00→ processor$0.79, flash$0.20, net$9.01.Gating
Auto-credit runs only if all hold:
FygaroConfig.credit.enabled— the existing yaml deploy-level master gate (kept as-is)auto_credit_enabledcurrency === "USD"gross_cents ≤ auto_credit_limit * 100(threshold on gross)net_cents > 0Otherwise → RECORD-ONLY: the ERPNext row stays
Fiat Received, nothing is credited, and one ops alert (notifyOpsEvent+alertBridge) names the specific failing gate:settings-unavailable/auto-credit-disabled/non-usd/over-limit/non-positive-net. Thecredit-disabledmaster gate records silently (unchanged deploy behavior; settings aren't even read).When it does credit: it sends
net_cents(not gross) and populates the row —initial_amount= gross,processor_fee,flash_fee,final_amount= net credited (fields added to theBridgeTransferRequestmodel/writer; the doctype fields ship in the companion PR).All existing safety is preserved: idempotency on the transaction id, Pending-means-credited, the releasing-lock + Completed-row processed-marker, and the
ResourceAttemptsLockServiceError-vs-other-error distinction.GraphQL —
globals.fygaroTopupNew nullable public field so the mobile app can compute "you'll receive $X" locally:
Sourced from the same 60s-cached settings. SDL regenerated via
yarn write-sdl(public schema + composed supergraph).Safe degradation
If ERPNext is unreachable or the settings row is malformed while credit is enabled, every payment is recorded, not credited, with a
settings-unavailableops alert — money is never credited off missing fee data, and the GraphQL field returnsnullso the app hides the estimate.Tests
test/flash/unit/services/fygaro/webhook-server/:initial/processor_fee/flash_fee/finalpopulated on credityarn tsc-check,yarn eslint-check, and the fullyarn test:unit(174 suites / 1487 tests) all pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb