Skip to content

fix(gpa): align all four wrappers with staging UAT shapes#41

Merged
boorad merged 2 commits into
mainfrom
fix/gpa-staging-shapes
May 17, 2026
Merged

fix(gpa): align all four wrappers with staging UAT shapes#41
boorad merged 2 commits into
mainfrom
fix/gpa-staging-shapes

Conversation

@boorad

@boorad boorad commented May 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix every client.gpa.* method against api-uat.ghin.com: prior to this PR, getAccesses() / requestAccess() / updateStatus() / revokeAccess() would all either reject the real response at the Zod layer or send a malformed request. Discovered while building the GPA opt-in flow in the spicy monorepo, which had to bypass the lib via httpClient.fetchCustomPath + z.unknown().
  • requestAccess() gains a required { email } second argument (breaking signature change — USGA rejects with 400 { errors: { email: ["can't be blank"] } } without it). All other inputs unchanged. Bumped to minor via changeset.
  • getAccesses() flattens the /users/accesses.json "UserAccesses" envelope (which also returns unrelated federations / associations / clubs / super_user / subtype siblings) to a clean Array<{ golferId, userAccessId, golferName, gpaStatus }>. IDs arrive as numeric strings on the wire and are Zod-coerced to number.

The five bugs, mapped to the fix

# Bug Fix
1 getAccesses schema expected { gpa_accesses: [] }; real shape is the UserAccesses envelope with string IDs and a gpa_status field schemaUserAccessesResponse matches the real wire shape; wrapper flattens golfers and coerces IDs
2 requestAccess sent empty body; USGA requires email Signature changed to requestAccess(golferId, { email })
3 requestAccess response expected { golfer_id, status }; real is { success: string } schemaGpaSuccessResponse
4 updateStatus response expected { golfer_id, status }; real is { success: string } schemaGpaSuccessResponse; also documented inline that user_id is the credentialed admin user's id from /users/login.json, not the golfer's user and not userAccessId
5 revokeAccess response expected { golfer_id }; real is { success: string } (with USGA's trailing space) schemaGpaSuccessResponse

Fixtures

Real staging responses captured under src/client/ghin/models/gpa/__fixtures__/. The new access.test.ts parses each fixture through its schema (catches drift loudly); the existing index.test.ts re-uses them through the mocked request-client.

Test plan

  • bun run test:run — 298 tests pass (10 new GPA model tests + 4 added wrapper tests covering email body, validation errors, flattening, and the empty-golfers case)
  • bun run lint — biome + tsc clean
  • bun run build — tsup produces dist with no errors
  • ./scripts/code-quality.sh — full pre-commit gate clean
  • Smoke-test against staging from the spicy app once 0.12.0 is published — drop the fetchCustomPath bypasses from packages/api/scripts/gpa-*.ts and confirm clean

Summary by CodeRabbit

Breaking Changes

  • GPA API client method signatures updated:
    • requestAccess() now requires email parameter (USGA requirement)
    • getAccesses() returns flattened array of golfer/access records
    • updateStatus(), requestAccess(), and revokeAccess() return standardized success response format

Review Change Stack

Every `client.gpa.*` method against api-uat.ghin.com either rejected the
real response at the Zod layer or sent a malformed request. Fixes all
five issues in one pass.

- `getAccesses()` now models `/users/accesses.json` correctly (USGA's
  "UserAccesses" envelope: federations / associations / clubs / golfers /
  super_user / subtype) and flattens the `golfers` branch — the only one
  carrying GPA state — into `Array<{ golferId, userAccessId, golferName,
  gpaStatus }>`. IDs arrive as numeric strings and are coerced to number.
- `requestAccess()` gains a required `{ email }` body parameter (breaking
  signature change — USGA returns 400 without it) and its response shape
  is corrected to `{ success: string }`.
- `updateStatus()` and `revokeAccess()` response schemas corrected to
  `{ success: string }`. Documented inline that `updateStatus`'s
  `user_id` is the credentialed admin user, not the golfer.

Fixtures captured from staging back the new schemas + flattening.
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@boorad has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 27 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b44036b-936a-4408-9133-d04bd87c51b4

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0179e and 4ed21b3.

📒 Files selected for processing (1)
  • src/client/ghin/index.test.ts
📝 Walkthrough

Walkthrough

This PR updates the GHIN client's GPA consent flow to match USGA staging UAT API behavior. It introduces new Zod schemas for the UserAccesses endpoint, refactors method implementations to flatten nested responses and enforce stricter request validation, and updates all corresponding tests and documentation to reflect breaking signature changes to requestAccess, getAccesses, updateStatus, and revokeAccess.

Changes

GPA API Alignment

Layer / File(s) Summary
GPA Schema Definitions and Fixtures
src/client/ghin/models/gpa/access.ts, src/client/ghin/models/gpa/__fixtures__/index.ts
Introduces schemaUserAccessesResponse to parse USGA /users/accesses.json with ID coercion, schemaGpaAccess for the flattened golfer record shape, schemaGpaRequestAccessRequest requiring non-empty email, schemaGpaSuccessResponse for mutation response envelopes, and schemaGpaUpdateStatusRequest restricting status to approved|denied. Four response fixtures capture staging API success messages.
GPA Schema Validation Tests
src/client/ghin/models/gpa/access.test.ts
Tests UserAccessesResponse parsing with string-to-number ID coercion, passthrough of unrelated fields, default empty golfers array, and multi-value gpa_status acceptance; validates GpaSuccessResponse envelope format across all mutation endpoints; ensures GpaRequestAccessRequest rejects missing/empty email; restricts GpaUpdateStatusRequest status values.
GPA Client Type and Schema Integration
src/client/ghin/index.ts
Updates imports to reference new schema types (GpaAccess, GpaRequestAccessRequest, GpaSuccessResponse, UserAccessesResponse); rewrites GhinClient.gpa public method signatures: getAccesses()Promise<GpaAccess[]>, requestAccess(golferId, request)Promise<GpaSuccessResponse>, updateStatus(request)Promise<GpaSuccessResponse>, revokeAccess(golferId)Promise<GpaSuccessResponse>.
GPA Client Method Implementations
src/client/ghin/index.ts
Rewrites gpaGetAccesses to fetch UserAccessesResponse and map/flatten golfers into a simple GpaAccess array; gpaRequestAccess validates email via schema, POSTs { email }, and returns success response; gpaUpdateStatus POSTs { gpa_status } extracted from request; gpaRevokeAccess DELETEs and returns success envelope; Zod errors converted to ValidationError with specific messages.
GPA Client Test Coverage
src/client/ghin/index.test.ts
Tests updated to use imported fixtures and validate new request/response shapes: getAccesses parses schema and returns flattened array; requestAccess POSTs JSON body with email; updateStatus includes gpa_status in body; revokeAccess returns success; added validation error cases (missing/invalid golfer ID, empty email).
GPA Documentation and Examples
.changeset/gpa-staging-shapes.md, src/playground/gpa-consent.ts
Changeset documents breaking changes: requestAccess signature, getAccesses endpoint/return shape, success response envelopes, and revokeAccess mutation semantics. Playground example updated to show requestAccess(golferId, { email }) usage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • boorad/ghin#28: Both PRs modify the GHIN GPA consent client implementation—especially the same gpa endpoints and their Zod request/response schemas/method contracts (e.g., getAccesses/requestAccess/updateStatus/revokeAccess).
  • boorad/ghin#31: Both PRs modify the GHIN client's GPA integration to match changed API response/request shapes—specifically aligning GPA "accesses"/"gpa_accesses" handling and gpa.updateStatus to use gpa_status in the request body.

Poem

🐰 The schemas now dance in a flattened array,
Email required for consent to stay,
Success envelopes ring true and bright,
Staging UAT aligned just right!
With coerced IDs and tests in place,
The GPA flow finds its perfect space. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: aligning GPA API wrappers with staging UAT response/request shapes to fix validation and request formatting issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@65a59b2). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #41   +/-   ##
=======================================
  Coverage        ?   95.81%           
=======================================
  Files           ?       54           
  Lines           ?     2530           
  Branches        ?      424           
=======================================
  Hits            ?     2424           
  Misses          ?      106           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/client/ghin/index.test.ts (1)

245-255: ⚡ Quick win

Keep the empty-accesses case on the same schema-validation path as production.

This test returns a raw envelope from mockFetch, while the happy-path case goes through schemaUserAccessesResponse.parse(...). That means this case can still pass with shapes the real RequestClient would reject, so it should parse the empty fixture through the schema too.

♻️ Proposed fix
       mockFetch.mockResolvedValue(
-        ok({
-          federations: [],
-          associations: [],
-          clubs: [],
-          golfers: [],
-          super_user: 'false',
-          subtype: null,
-        }),
+        ok(
+          schemaUserAccessesResponse.parse({
+            federations: [],
+            associations: [],
+            clubs: [],
+            golfers: [],
+            super_user: 'false',
+            subtype: null,
+          }),
+        ),
       )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/ghin/index.test.ts` around lines 245 - 255, The test is returning
a raw envelope from mockFetch instead of validating it the same way production
does; update the test so the empty fixture is fed through the same validator by
calling schemaUserAccessesResponse.parse(...) (or otherwise passing the parsed
result) before resolving mockFetch, so the empty-accesses case follows the same
schema-validation path as the happy-path that uses
schemaUserAccessesResponse.parse; locate usages of mockFetch.mockResolvedValue
in this test and replace the raw object with the parsed/validated value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/ghin/index.ts`:
- Around line 123-127: The GPA API methods (gpa.getAccesses, gpa.requestAccess,
gpa.updateStatus, gpa.revokeAccess) and their GhinClient implementations
currently return Promise<T> and throw on error; change their signatures to
return Promise<Result<T, E>> (use neverthrow Result) with appropriate error type
(e.g., GpaError) — update the GhinClient methods to catch internal exceptions
and return Result.ok(value) on success or Result.err(error) on failure instead
of throwing, and propagate these Result types through any callers and exported
types so the public surface no longer throws but returns Result values.

---

Nitpick comments:
In `@src/client/ghin/index.test.ts`:
- Around line 245-255: The test is returning a raw envelope from mockFetch
instead of validating it the same way production does; update the test so the
empty fixture is fed through the same validator by calling
schemaUserAccessesResponse.parse(...) (or otherwise passing the parsed result)
before resolving mockFetch, so the empty-accesses case follows the same
schema-validation path as the happy-path that uses
schemaUserAccessesResponse.parse; locate usages of mockFetch.mockResolvedValue
in this test and replace the raw object with the parsed/validated value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40818890-c3f9-4376-9159-19a764d3ad3c

📥 Commits

Reviewing files that changed from the base of the PR and between 65a59b2 and 0e0179e.

📒 Files selected for processing (7)
  • .changeset/gpa-staging-shapes.md
  • src/client/ghin/index.test.ts
  • src/client/ghin/index.ts
  • src/client/ghin/models/gpa/__fixtures__/index.ts
  • src/client/ghin/models/gpa/access.test.ts
  • src/client/ghin/models/gpa/access.ts
  • src/playground/gpa-consent.ts

Comment thread src/client/ghin/index.ts
@boorad

boorad commented May 17, 2026

Copy link
Copy Markdown
Owner Author

Responding to CodeRabbit nitpick on src/client/ghin/index.test.ts lines 245-255 (empty-accesses case):

Fixed — the empty fixture is now routed through schemaUserAccessesResponse.parse(...) so it follows the same schema-validation path as the happy-path test. Good catch.

Routes the empty-fixture case through schemaUserAccessesResponse.parse(...)
to mirror the production path, so the test fails on shapes the real
RequestClient would reject. Addresses CodeRabbit nitpick on #41.
@boorad boorad merged commit 8015734 into main May 17, 2026
7 checks passed
@boorad boorad deleted the fix/gpa-staging-shapes branch May 17, 2026 17:26
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.

1 participant