Sync affiliate counts on application status changes#202
Conversation
Greptile SummaryThis PR adds
Confidence Score: 3/5Not safe to merge: the offer select in the PATCH handler still omits total_affiliates, so the counter will be set to 0 or 1 on every approve/reject regardless of its real value. The PATCH handler reads the offer row with .select("id, seller_id") (line 105), leaving total_affiliates out. Every call to updateAffiliateCountForStatusChange therefore receives undefined as currentCount, which the || 0 fallback silently converts to 0 — so every approval writes 1 and every rejection writes 0, permanently destroying the real counter. This bug was noted in the previous review pass and is still present in the current diff. route.ts line 105 — the offer select must include total_affiliates before this change can function correctly Important Files Changed
Reviews (3): Last reviewed commit: "Sync affiliate count on application stat..." | Re-trigger Greptile |
| const totalAffiliates = Math.max(0, (currentCount || 0) + delta); | ||
|
|
||
| const { error } = await admin | ||
| .from("affiliate_offers") | ||
| .update({ | ||
| total_affiliates: totalAffiliates, | ||
| updated_at: new Date().toISOString(), | ||
| }) | ||
| .eq("id", offerId); |
There was a problem hiding this comment.
Read-modify-write race on
total_affiliates
Two concurrent PATCH requests that both approve different applications for the same offer will both read the same total_affiliates snapshot, compute count + 1 independently, and both write the same resulting value — so one increment is silently lost. A database-level atomic update (e.g. a Postgres total_affiliates + 1 expression or an RPC with FOR UPDATE row lock) would eliminate this window.
| total_affiliates: options.totalAffiliates, | ||
| }, | ||
| error: null, | ||
| }); | ||
| } | ||
| return query(table, { data: null, error: null }); | ||
| } | ||
|
|
||
| if (table === "affiliate_applications") { | ||
| if (applicationCalls++ === 0) { | ||
| return query(table, { | ||
| data: { | ||
| id: "app-1", | ||
| status: options.previousStatus, | ||
| }, | ||
| error: null, |
There was a problem hiding this comment.
Mock returns
total_affiliates regardless of the select fields, masking the production bug
mockPatchQueries always includes total_affiliates in the offer data no matter what columns are actually selected by the route. As a result all four tests pass even when the route's offer query omits total_affiliates, giving a false green signal. Tightening the mock (e.g. by only returning fields that match the select string, or by adding an assertion that the select call includes total_affiliates) would let the test suite catch this class of regression.
Fixes #201.
Summary
total_affiliateswhen an application becomes approvedtotal_affiliateswhen an approved application is rejected, clamped at zeroValidation
./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 falsegit diff --check -- src/app/api/affiliates/offers/[id]/applications/route.ts src/app/api/affiliates/offers/[id]/applications/route.test.ts