Skip to content

Handle affiliate approval notification failures#204

Closed
morganschp wants to merge 2 commits into
profullstack:masterfrom
morganschp:fix-affiliate-approval-notification-failure
Closed

Handle affiliate approval notification failures#204
morganschp wants to merge 2 commits into
profullstack:masterfrom
morganschp:fix-affiliate-approval-notification-failure

Conversation

@morganschp
Copy link
Copy Markdown
Contributor

@morganschp morganschp commented May 21, 2026

Fixes #203.
Fixes #163.

Summary

  • treat affiliate application approval/rejection notifications as best-effort after the status update succeeds
  • reject malformed or non-object affiliate application status request bodies before offer/application update or notification work
  • log notification insert errors or thrown failures instead of returning a false 500 to the seller UI
  • add route regression coverage for malformed JSON, non-object JSON, and a failed notification insert

uGig task

Submitted for the active uGig repo testing task: https://ugig.net/gigs/4741218f-a723-46bb-82cb-6516120331ae

No payout details included; those can be provided after acceptance.

Verification

  • ./node_modules/.bin/vitest run 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'
  • ./node_modules/.bin/eslint 'src/app/api/affiliates/offers/[id]/applications/route.ts' 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'
  • ./node_modules/.bin/prettier --check 'src/app/api/affiliates/offers/[id]/applications/route.ts' 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'
  • ./node_modules/.bin/tsc --noEmit --pretty false
  • git diff --check -- 'src/app/api/affiliates/offers/[id]/applications/route.ts' 'src/app/api/affiliates/offers/[id]/applications/route.test.ts'

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 23, 2026

Greptile Summary

This PR hardens the affiliate application approval/rejection endpoint by treating notification delivery as best-effort and rejecting malformed or non-object request bodies before any database work occurs.

  • route.ts: Body parsing is now wrapped in a dedicated try/catch returning 400 on malformed JSON; an object-shape guard rejects null and arrays; application_id validation is tightened to a typeof === "string" check; the notification insert is wrapped in its own try/catch so any failure is logged via console.warn and the 200 response is still returned to the seller UI.
  • route.test.ts: New test file adds regression coverage for malformed JSON, non-object JSON, and the notification-insert-error path using a Proxy-based chainable Supabase mock.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to input validation and notification error handling, both of which improve resilience without altering the successful status-update path.

All three new failure modes (malformed JSON, non-object body, failed notification insert) are guarded correctly and covered by tests. The status-update path is unchanged. No auth or data-integrity regressions were introduced.

No files require special attention.

Important Files Changed

Filename Overview
src/app/api/affiliates/offers/[id]/applications/route.ts Wraps body parsing in try/catch to return 400 on malformed JSON, adds object-shape guard, tightens application_id to string-type check, and demotes notification insert to best-effort with console.warn on failure.
src/app/api/affiliates/offers/[id]/applications/route.test.ts New test file covering malformed JSON, non-object JSON, and the notification-insert-error path; uses a Proxy-based chainable mock for Supabase.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant R as PATCH Route
    participant DB as Supabase (DB)
    participant N as Notifications Table

    C->>R: PATCH /api/affiliates/offers/[id]/applications
    R->>R: Parse JSON body (try/catch → 400 on failure)
    R->>R: Validate body is non-null object (→ 400)
    R->>R: "Validate application_id (string) and action (approve|reject) (→ 400)"
    R->>DB: Verify seller ownership (affiliate_offers)
    DB-->>R: offer or null
    alt not owner
        R-->>C: 404 Not found or not authorized
    end
    R->>DB: Update affiliate_applications (status, updated_at, approved_at)
    DB-->>R: updated application or error
    alt update error
        R-->>C: 400 error.message
    end
    Note over R,N: Status already committed — notification is best-effort
    R->>N: insert notification (try/catch)
    alt notification error / throw
        R->>R: console.warn (swallow error)
    end
    R-->>C: "200 { application }"
Loading

Reviews (3): Last reviewed commit: "Validate affiliate application status bo..." | Re-trigger Greptile

Comment on lines +82 to +143
it("still returns the updated application when notification insert fails", async () => {
mockGetAuthContext.mockResolvedValue({
user: { id: "seller-1", authMethod: "session" },
});

const notificationInsert = vi.fn().mockResolvedValue({
error: { message: "notification table unavailable" },
});
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});

mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return chainable({ id: "offer-1", seller_id: "seller-1" });
}

if (table === "affiliate_applications") {
return chainable({
id: "app-1",
affiliate_id: "affiliate-1",
offer_id: "offer-1",
status: "approved",
profiles: { username: "alice" },
});
}

if (table === "notifications") {
return { insert: notificationInsert };
}

return chainable(null);
});

try {
const res = await PATCH(
makePatchRequest("offer-1", {
application_id: "app-1",
action: "approve",
}),
makeParams("offer-1")
);

expect(res.status).toBe(200);
const body = await res.json();
expect(body.application).toMatchObject({
id: "app-1",
status: "approved",
});
expect(notificationInsert).toHaveBeenCalledWith(
expect.objectContaining({
user_id: "affiliate-1",
type: "affiliate_approved",
data: { offer_id: "offer-1", application_id: "app-1" },
})
);
expect(consoleWarn).toHaveBeenCalledWith(
"Failed to create affiliate application notification",
{ message: "notification table unavailable" }
);
} finally {
consoleWarn.mockRestore();
}
});
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 Thrown-exception path of notification catch block is untested

The new notification wrapping has two distinct failure modes: notificationError returned in the result (tested here) and an exception thrown by .insert() itself (the catch (error) branch at line 140 of route.ts). Only the first path is covered. If .insert were to throw synchronously or reject, the catch branch would fire and the route would still return 200, but that invariant is never verified. A small additional test case — where notificationInsert rejects instead of resolving with an error — would complete the coverage for this new behavior.

@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@ralyodio ralyodio closed this May 23, 2026
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.

bug: affiliate approval can return 500 when notification write fails bug: affiliate application status API returns 500 on malformed JSON

2 participants