Skip to content

feat(fygaro): payment webhook — record, notify, and flag-gated auto-credit - #472

Merged
islandbitcoin merged 3 commits into
mainfrom
feat/fygaro-webhook
Aug 8, 2026
Merged

feat(fygaro): payment webhook — record, notify, and flag-gated auto-credit#472
islandbitcoin merged 3 commits into
mainfrom
feat/fygaro-webhook

Conversation

@islandbitcoin

Copy link
Copy Markdown
Contributor

Why

Fygaro card top-ups leave no server-side record: the app opens Fygaro's hosted payment button and nothing ever comes back to Flash. Attribution requires reading a PayPal receipt email, and crediting is a manual treasury transfer. This implements the missing recording path from the TopUp V1 design, mirroring the Bridge webhook server structure throughout. Companion app fix: lnflash/flash-mobile#685 (sends the username in custom_reference — the parameter Fygaro actually records).

What

New fygaro-webhook server (default port 4010, yarn fygaro-webhook), mirroring bridge-webhook-server:

  • raw-body capture, /health, enabled guard (503 when fygaro.enabled is off), per-sender rate limiting
  • HMAC-SHA-256 signature verification per Fygaro's hook spec: Fygaro-Signature: t=<ts>,v1=<hex>[,v1=…] over ${timestamp}.${rawBody}, Fygaro-Key-ID selects the shared secret, multiple secrets supported for rotation, timestamp-skew rejection, timing-safe compare

POST /payment handler:

  • attributes the payment via customReference → username → account
  • writes the ERPNext audit row (Bridge Transfer Request, provider=Fygaro, fygaro:<transactionId>, Fiat Received) for every payment — unattributed ones (blank/unknown reference, i.e. every pre-fix app build) are recorded and alert ops with the payer email instead of being silently dropped
  • posts to the Discord ops activity feed (deposit flow)
  • Redis idempotency lock taken after persistence succeeds (same recovery stance as the Bridge deposit handler)

Flag-gated auto-credit (fygaro.credit.enabled, default OFF):

  • USD payments are credited from the bankowner treasury to the user's cash wallet (USDT-preferred, USD fallback — same wallet resolution as the referral-reward payout) via intraledgerPaymentSendWalletIdForUsdWallet
  • idempotent on fygaro:<transactionId> (withPaymentIdempotency, ENG-530 pattern); Pending sends are treated as credited, never retried
  • on success the audit row is promoted to Completed with the credited wallet; on failure it stays at Fiat Received with a critical alert for manual follow-up
  • non-USD payments are never auto-credited (warning alert)

Config: fygaro block in the YAML schema (enabled, webhook.port/secrets/timestampSkewMs, credit.enabled) with a default-off baseline; AlertSource gains fygaro-webhook; new dedup keys.

Rollout

  1. Merge + deploy with fygaro.enabled: false (baseline) — no behavior change
  2. Configure the hook URL + API credential in Fygaro's dashboard (payment button → Advanced Options), put the shared secret in fygaro.webhook.secrets via config overrides, enable fygaro.enabled — payments now recorded + ops-notified, crediting stays manual
  3. After a few clean weeks, flip fygaro.credit.enabled — full automation

Tests

test/flash/unit/services/fygaro/webhook-server/:

  • verify-signature.spec.ts — valid sig (key id / no key id / rotation with multiple v1 / ms timestamps), wrong sig, tampered body, missing/malformed header, stale timestamp, no secrets configured, missing raw body
  • enabled-guard.spec.ts — 503 when disabled, /health exempt, pass-through when enabled
  • payment.spec.ts — full handler matrix: 400 validation, attributed record with credit off, blank/unknown reference → recorded + unattributed alert, ERPNext failure → 500 for provider retry, duplicate delivery short-circuit, credit success (cents conversion + promotion), credit failure (critical alert, no promotion), non-USD guard, promotion-failure-after-credit still reports success

yarn tsc-check clean, yarn eslint-check clean, full unit suite green (168 suites / 1420 tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb

…redit

Fygaro card top-ups previously left no server-side record: the app opened
Fygaro's hosted payment button and nothing ever came back to Flash, so
every top-up required manual PayPal-email archaeology to attribute and a
manual treasury transfer to credit. This implements the missing recording
path (TopUp V1 design), mirroring the Bridge webhook server structure.

- fygaro-webhook server (default port 4010): raw-body capture, /health,
  enabled guard, rate limit, HMAC-SHA-256 signature verification per
  Fygaro's hook spec (Fygaro-Signature t=..,v1=..; Fygaro-Key-ID selects
  the shared secret; multiple secrets supported for rotation)
- /payment handler: attributes the payment via customReference (Flash
  username), writes the ERPNext audit row (Bridge Transfer Request,
  provider=Fygaro, Fiat Received) for every payment — including
  unattributed ones, which alert ops with the payer email instead of
  being silently dropped — and posts to the Discord ops activity feed
- fygaro.credit.enabled (default OFF): when on, USD payments are
  auto-credited from the bankowner treasury to the user's cash wallet via
  intraledger send, idempotent on the Fygaro transaction id (ENG-530
  pattern), then the audit row is promoted to Completed. Credit failures
  alert critical and leave the row at Fiat Received for manual follow-up.
- config: fygaro block (enabled / webhook.port / webhook.secrets /
  webhook.timestampSkewMs / credit.enabled), default-off baseline
- unit tests: signature verification, enabled guard, and the full payment
  handler matrix (attribution, dedupe, credit on/off, non-USD, failures)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb
…, hardening

Addresses the strict-review findings on the payment webhook:

- credit path now serializes deliveries with a RELEASING lock
  (lockPaymentIdempotencyKey) and uses the audit row's Completed status
  as the processed marker, instead of consuming a non-releasing timelock
  before the send. A crash mid-credit releases the lock so the next
  provider retry re-runs the credit — withPaymentIdempotency keeps the
  send exactly-once — eliminating the stranded paid-but-uncredited
  window. Promotion failures now also self-heal on retry.
- new isFygaroTopupCompleted helper (degrades to false on lookup failure;
  a false negative replays the cached send, never double-pays)
- unattributed path takes the dedupe timelock before emitting, so Fygaro
  re-deliveries no longer spam a fresh ops-feed line per attempt
- enabled guard optional-chains FygaroConfig (fail closed 503, never a
  per-request throw); signature verify documents the hex-digest
  assumption to check against the first real signed payment
- NEW: credit-topup.spec.ts — direct tests for the money-moving path:
  treasury role/wallet resolution, USDT-preference + USD fallback,
  recipient currency matching, cents validation matrix, Pending
  semantics, send errors, unexpected-status rejection
- payment.spec.ts updated for the new lock semantics: Completed-row
  short-circuit, incomplete-attempt re-run, lock contention, and
  unattributed re-delivery dedupe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb
…k throw

redlock reports a throw from inside the wrapped callback as a generic
lock error; acking that as "already_processing" (200) would stop Fygaro's
retries and strand the payment at Fiat Received with no alert. Only
ResourceAttemptsLockServiceError (real contention) is acked now — any
other lock error is rethrown so the catch-all returns 500 + critical
alert and the provider retries. Test added for the throw path; the
contention test now uses the real error class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb
@islandbitcoin
islandbitcoin merged commit b065e32 into main Aug 8, 2026
15 checks passed
islandbitcoin added a commit to lnflash/frappe-flash-admin that referenced this pull request Aug 8, 2026
…ests (#60)

* feat(admin): accept + surface Fygaro provider on Bridge Transfer Requests

Companion to lnflash/flash#472 (Fygaro card top-up webhook), which writes
Bridge Transfer Request rows with provider=Fygaro.

- provider Select options: Bridge -> Bridge\nFygaro. Without this, every
  Fygaro audit insert fails Frappe's server-side Select validation, the
  webhook 500s, and no top-up is ever recorded.
- provider added to the doctype list view and shown on the Transfer
  Requests page: a Provider column on the bridge tab and a Provider row
  in the detail drawer (existing rows default-display as Bridge). The
  page's query already returns the field; no API change needed.
- doctype `modified` stamp bumped so bench migrate actually syncs the
  new option.

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

* style: prettier formatting for bridgeHeaders array (pre-commit)

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

* feat(admin): dedicated Card Top-Ups tab — Fygaro records off the Bridge tab

Putting card top-ups under "Bridge" was counter-intuitive. The Transfer
Requests page now has three tabs: Cashouts, Bridge, and Card Top-Ups.

- Card Top-Ups tab lists Bridge Transfer Request rows with
  provider=Fygaro; the Bridge tab is now provider=Bridge only
  (get_bridge_transfer_requests gains a provider filter)
- per-tab pulse tiles: Fygaro shows Fiat Received (awaiting credit),
  Completed, Failed — counts provider-scoped in get_transfer_pulse so
  the Bridge tiles no longer mix in card top-ups
- per-tab titles, placeholders, and empty states; the transaction-type
  filter stays Bridge-only (card top-ups are Topup by definition)
- the redundant Provider table column is dropped (the tab implies it);
  the detail drawer keeps the Provider row and titles itself
  "Card Top-Up Details" for Fygaro rows

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

* style: ruff formatting for pulse.py (pre-commit)

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

---------

Co-authored-by: Dread <dread@example.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
islandbitcoin pushed a commit to lnflash/charts that referenced this pull request Aug 8, 2026
…chart 3.2.53)

Companion to lnflash/flash#472 (Fygaro card top-up payment webhook).
Mirrors the bridge-webhook workload 1:1: same env surface, config secret
mount, probes, and enabled-gating.

- templates: fygaro-webhook-deployment/-service/fygaro-ingress, rendered
  only when galoy.fygaro.webhook.enabled (default off)
- values: galoy.fygaro.webhook workload block (port 4010) and the
  `fygaro` app-config block under galoy.config (default-off; HMAC
  secrets injected per environment via deployments overlays)
- helper: galoy.fygaro.webhook.fullname

Render-verified: enabled=true produces all three objects with the
fygaro-webhook-server entrypoint; default renders no workload while the
fygaro config still reaches custom.yaml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcEjF6SS3Ci5D4CzZqdPjb
islandbitcoin added a commit to lnflash/charts that referenced this pull request Aug 8, 2026
…chart 3.2.54) (#253)

* feat(flash): fygaro-webhook workload — deployment, service, ingress (chart 3.2.53)

Companion to lnflash/flash#472 (Fygaro card top-up payment webhook).
Mirrors the bridge-webhook workload 1:1: same env surface, config secret
mount, probes, and enabled-gating.

- templates: fygaro-webhook-deployment/-service/fygaro-ingress, rendered
  only when galoy.fygaro.webhook.enabled (default off)
- values: galoy.fygaro.webhook workload block (port 4010) and the
  `fygaro` app-config block under galoy.config (default-off; HMAC
  secrets injected per environment via deployments overlays)
- helper: galoy.fygaro.webhook.fullname

Render-verified: enabled=true produces all three objects with the
fygaro-webhook-server entrypoint; default renders no workload while the
fygaro config still reaches custom.yaml.

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

* fix(flash): keep fygaro out of default galoy.config — old images crash on unknown keys

The app config schema is additionalProperties:false at the root, so the
currently-pinned image rejects the fygaro key in custom.yaml and every
config-loading pod crash-loops (api/trigger/websocket/exporter/
ibex-webhook) — which is exactly what the dev-setup smoketest caught.

The fygaro config block moves to deployments overlays, added only after
the flash image includes the fygaro schema (which defaults the feature
off when the key is absent). The chart keeps only the workload block,
which renders nothing while disabled.

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

---------

Co-authored-by: Dread <dread@example.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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