Skip to content

Return 400 for malformed affiliate payout JSON#457

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
rissrice2105-agent:codex/affiliate-pay-json-400
Jun 14, 2026
Merged

Return 400 for malformed affiliate payout JSON#457
ralyodio merged 1 commit into
profullstack:masterfrom
rissrice2105-agent:codex/affiliate-pay-json-400

Conversation

@rissrice2105-agent

Copy link
Copy Markdown
Contributor

Follow-up for #169.

Summary

  • Parse affiliate conversion payout JSON before service-client ownership/conversion queries
  • Return 400 for malformed JSON instead of falling through to the generic 500 handler
  • Add regression coverage that confirms malformed JSON does not query affiliate offers, wallets, or transfers

Verification

  • vitest run src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts
  • tsc --noEmit

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a regression from #169 where a malformed JSON request body would bypass the 400 path and fall through to the generic 500 handler. The fix wraps request.json() in its own try/catch and moves body parsing — along with conversion_id validation — ahead of the Supabase service-client creation and ownership queries.

  • Route (route.ts): JSON parsing and conversion_id validation are now the first things that run after the auth check; malformed JSON returns { error: "Invalid JSON body" } with status 400 before any DB calls are made.
  • Test (route.test.ts): A new test (makeRawPostRequest) confirms that mockFrom, mockGetUserLnWallet, and mockInternalTransfer are all untouched when the body is not valid JSON; the existing non-string conversion_id test continues to pass but its affiliate_offers mock setup is now dead code since the ownership check no longer runs on that path.

Confidence Score: 4/5

Safe to merge; the core fix is correct and the new test verifies no DB calls occur on malformed input.

The JSON-parsing fix is straightforward and well-tested. The two items worth a second look are: the stale mockFrom setup in the existing conversion_id test (misleads readers about control flow) and the reordering of validation before the ownership gate (authenticated non-owners now see 400 for bad input rather than 403, exposing API contract details they shouldn't need to know). Neither affects data integrity or access control, but the ordering change is a deliberate behavior difference that deserves a comment or intentional design decision.

Both changed files are worth a quick look: route.ts for the validation-before-authorization ordering, and route.test.ts for the now-unreachable affiliate_offers mock in the non-string conversion_id test case.

Important Files Changed

Filename Overview
src/app/api/affiliates/offers/[id]/conversions/pay/route.ts JSON parsing moved before DB calls so malformed bodies now return 400; side-effect is that conversion_id validation also runs before the ownership check, altering the error code non-owners observe.
src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts New regression test correctly verifies 400 and no DB calls for malformed JSON; existing non-string conversion_id test retains stale mockFrom setup for affiliate_offers that is no longer reachable under the new control flow.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as POST /conversions/pay
    participant Auth as getAuthContext
    participant DB as Supabase (admin)
    participant LN as Lightning (wallet-utils)

    Client->>Route: "POST {body}"
    Route->>Auth: getAuthContext(request)
    Auth-->>Route: auth / null

    alt No auth
        Route-->>Client: 401 Unauthorized
    end

    Note over Route: NEW: parse body here
    alt Malformed JSON
        Route-->>Client: 400 Invalid JSON body
    end

    alt invalid / missing conversion_id
        Route-->>Client: 400 conversion_id must be non-empty string
    end

    Route->>DB: affiliate_offers → select seller_id
    alt Not owner
        Route-->>Client: 403 Not authorized
    end

    Route->>DB: affiliate_conversions → select conversion
    alt Not found / already paid / clawed back / unsettled / zero commission
        Route-->>Client: 400 / 404
    end

    Route->>LN: getUserLnWallet(seller)
    Route->>LN: getUserLnWallet(affiliate)
    Route->>LN: internalTransfer(seller→affiliate)
    Route->>DB: "update affiliate_conversions (status=paid)"
    Route->>DB: insert wallet_transactions (x2)
    Route-->>Client: "200 { ok: true, commission_sats }"
Loading

Comments Outside Diff (1)

  1. src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts, line 87-114 (link)

    P2 Stale ownership mock in existing test

    The mockFrom setup for affiliate_offers (lines 92-99) is now dead code. Since conversion_id validation was moved before createServiceClient(), the ownership check never runs in this test path — mockFrom is never invoked at all. The test still passes because expect(mockFrom).not.toHaveBeenCalledWith("affiliate_conversions") is trivially true, but a future reader will incorrectly infer that the ownership check runs before the validation check. Consider either removing the stale mock setup or adding expect(mockFrom).not.toHaveBeenCalled() (as the new malformed-JSON test does) to correctly document the current control flow.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "Handle malformed affiliate payout JSON" | Re-trigger Greptile

Comment on lines +24 to +41
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const conversion_id =
body && typeof body === "object" && !Array.isArray(body)
? (body as Record<string, unknown>).conversion_id
: undefined;

if (typeof conversion_id !== "string" || conversion_id.trim() === "") {
return NextResponse.json(
{ error: "conversion_id must be a non-empty string" },
{ status: 400 }
);
}
const conversionId = conversion_id.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Authorization check now runs after input validation

Moving body parsing and conversion_id validation above the ownership check changes the observable behavior for non-owners. Previously any authenticated user who didn't own the offer received 403 regardless of body content. Now they receive 400 for malformed JSON or an invalid conversion_id, and only reach 403 when the JSON is well-formed with a string conversion_id. The conventional order (authenticate → authorize → validate) avoids leaking API contract details to callers who have no business touching the resource. The fix is still correct for the stated goal, but the ordering could be tightened by moving body parsing after the offer.seller_id ownership gate — or at minimum noting the trade-off.

@rissrice2105-agent

Copy link
Copy Markdown
Contributor Author

CI is green for PR #457.

Verification:

  • vitest run src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts
  • tsc --noEmit

uGig invoice evidence has been sent for this PR.

@ralyodio ralyodio merged commit ffcc084 into profullstack:master Jun 14, 2026
6 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