Skip to content

fix(fygaro): serve the v0.6.7 allowance query instead of 400ing it - #492

Merged
islandbitcoin merged 3 commits into
mainfrom
fix/fygaro-allowance-legacy-fields
Aug 21, 2026
Merged

fix(fygaro): serve the v0.6.7 allowance query instead of 400ing it#492
islandbitcoin merged 3 commits into
mainfrom
fix/fygaro-allowance-legacy-fields

Conversation

@bobodread876

Copy link
Copy Markdown
Collaborator

Every card top-up amount screen on v0.6.7 shows an error toast reading "StatusCode: 400 / Error code: undefined". Reproduced against prod.

What v0.6.7 sends

query fygaroTopupAllowance {
  fygaroTopupAllowance { limit held remaining holdsExpireAt }
}

Those are fields of FygaroTopupAllowance. The query field returns FygaroTopupAllowancePayload (allowance, errors, unavailableReason), so the server answers 400 GRAPHQL_VALIDATION_FAILED — and the app's global Apollo handler turns any 4xx into a toast.

$ curl -s -o /dev/null -w '%{http_code}' api.flashapp.me/graphql -d @v067-query.json
400

How it shipped

Codegen ran against a checked-in schema that still described the pre-payload shape. flash-mobile's app/graphql/public-schema.graphql:1857 in the v0.6.7 tree still reads:

fygaroTopupAllowance: FygaroTopupAllowance

The generator validated happily against a schema the server had already moved past. Codegen is only as honest as the schema you check in — a CI check that the committed SDL matches the deployed one is the real fix for the class, and belongs in flash-mobile.

Why fix it server-side

Aliasing the four fields onto the payload fixes every already-installed v0.6.7 with no store release. Those users cannot be reached any other way, and v0.6.7 went public today. All four carry @deprecated and get deleted once that build ages out.

Why limit/held/remaining are NON-NULL

This is the subtle part, and it is the opposite of the instinct.

When the allowance is unavailable (ERPNext unreadable, rate limited, checkout disabled) they resolve to null, the non-null violation propagates to the root, and data goes null. That is exactly what the app already handles: data?.fygaroTopupAllowance is undefined, so it falls back to rendering the flat per-level cap — the same thing it does today when the query 400s.

Nullable would be worse. use-card-topup-allowance.ts builds its allowance object from any truthy payload, so nulls would produce {limitCents: null, remainingCents: null} and the screen would tell a customer they have nothing left to spend. A false refusal in place of a graceful fallback.

And the propagation produces a GraphQL error on a 200, not a network error — the app logs those and never toasts them ("only network error are managed globally", client.tsx). So the fallback is silent in exactly the case that needs it.

Severity

Always cosmetic: the allowance degraded to the flat cap and top-ups worked throughout — which is why signed checkout tested clean. But it put a raw status code in front of a customer on a money screen, every time.

Tests

test/flash/unit/graphql/public/types/object/fygaro-allowance-legacy-fields.spec.ts holds the v0.6.7 document as a frozen literal — copied from the shipped tag, not regenerated, because its whole value is being a copy of what a real build sends. Regenerating it from the current schema would make it agree with us by construction and assert nothing.

Four cases: it validates; it serves the values; an unavailable allowance nulls data rather than zeroing it; and the modern payload shape is undisturbed.

The schema is built around the real payload type rather than from SDL — buildSchema would give default resolvers that read payload.limit directly and pass whether or not the aliasing resolvers exist.

205 suites / 2219 tests green. tsc and eslint clean. SDL and supergraph regenerated via yarn write-sdl.

bobodread876 and others added 3 commits August 21, 2026 13:20
Every card top-up amount screen on v0.6.7 shows an error toast reading
"StatusCode: 400 / Error code: undefined". Reproduced against prod.

v0.6.7 queries the payload as if it WERE the allowance:

  fygaroTopupAllowance { limit held remaining holdsExpireAt }

Those are fields of FygaroTopupAllowance. The query field returns
FygaroTopupAllowancePayload, so the server answers 400
GRAPHQL_VALIDATION_FAILED, and the app's global Apollo handler turns any
4xx into a toast.

It shipped because codegen ran against a checked-in public-schema.graphql
that still described the pre-payload shape — flash-mobile's copy still
reads "fygaroTopupAllowance: FygaroTopupAllowance" at line 1857 of the
v0.6.7 tree. The generator validated happily against a schema the server
had already moved past.

Aliasing the four fields onto the payload fixes every INSTALLED v0.6.7
without a store release, which is the entire point: those users cannot be
reached any other way. All four are deprecated and go when v0.6.7 ages
out.

limit/held/remaining are NON-NULL deliberately. When the allowance is
unavailable they resolve to null, the violation propagates to the root,
and `data` goes null — which is exactly what the app already handles by
falling back to the flat per-level cap. Nullable would be worse: v0.6.7
builds its allowance from any truthy payload, so nulls would render as a
zeroed allowance and tell the customer they have nothing left to spend.

The propagation produces a GraphQL error on a 200, not a network error,
and the app logs those without toasting ("only network error are managed
globally", flash-mobile app/graphql/client.tsx). So the fallback is
silent, which is the case that needs it.

Functionally this was always cosmetic — the allowance degraded to the
flat cap and top-ups worked — but it put a raw status code in front of a
customer on a money screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEoz7nBtdtsHyuYG5wNPQV
…e shim path

Review fixes for PR #492:

- AllowancePayloadSource.holdsExpireAt now declares the runtime union
  (Date | number | null): the app layer returns holdsExpireAt?: Date and
  the root resolver passes it through untouched, so the previous
  number | null annotation was factually wrong and invited a 1000x
  ms-vs-seconds bug for anyone comparing it numerically. A comment warns
  against numeric comparison.

- Add a test for the legacy path's most common production case:
  allowance present, nothing held, holdsExpireAt absent — pinning that
  the shim serves holdsExpireAt: null with no errors instead of
  propagating.

- Add a test that a Date holdsExpireAt (the actual runtime type)
  serializes to Unix seconds through the flat fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEoz7nBtdtsHyuYG5wNPQV
Verified by execution before writing this: a document that selects a
deprecated flat field ALONGSIDE the modern payload shape loses everything
when the allowance is unavailable. The non-null violation on the flat
field propagates through the NonNull root and nulls data entirely —
unavailableReason with it, the one field whose purpose is separating
"hide the option" from "retry". A transitional query mixing both shapes
is precisely the likeliest next query someone writes while migrating off
these fields, and it would silently reinstate the invite-then-refuse loop
the payload type exists to end.

The shim docblock now states the rule — v0.6.8+ queries ONLY the payload
shape, these four exist for the frozen v0.6.7 document and nothing else —
and the spec pins the trap as a documented property rather than a
discovery: mixed selection + unavailable allowance -> data null, errors
present.

7/7 tests green, tsc and eslint clean. No schema change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEoz7nBtdtsHyuYG5wNPQV
@islandbitcoin
islandbitcoin merged commit 892346e into main Aug 21, 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