feat(swap-service): stable partner attribution via partnerCode#44
Conversation
Attribute swaps by a stable partnerCode (FK to the affiliate registry)
instead of the mutable partnerAddress payout snapshot. Address changes
no longer fragment a partner's history, and shared receive addresses
can no longer mix two partners' swaps.
- schema: add Swap.partnerCode as a real relation to Affiliate.partnerCode
(now NOT NULL); keep partnerAddress as a per-swap payout snapshot
- migration: backfill partnerCode from partnerAddress (unambiguous matches
only), enforce affiliate code NOT NULL, add FK + index; partnerAddress
retained for the legacy window
- write path: resolvePartner stamps a registry-resolved code plus the
payout-address snapshot; unknown/absent codes fall back to a raw address
- reads: attribute through the affiliate relation / partnerCode, never
partnerAddress; add code-keyed getAffiliate{Stats,Swaps}ByPartnerCode
- /v1/affiliate/{swaps,stats}: accept partnerCode (preferred) or address
during the migration window
- drop the dead partnerAddress passthrough in the AVNU verifier stub
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 53 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds partnerCode-based attribution across swap creation, affiliate read endpoints, affiliate fee calculation, Prisma schema/migration, verification payloads, and test coverage, while keeping partnerAddress support for address-based lookups. ChangesPartner code attribution flow
Sequence Diagram(s)sequenceDiagram
participant Client
participant AffiliateController
participant AffiliateService
participant PrismaService
Client->>AffiliateController: GET stats or swaps with partnerCode or address
alt partnerCode present
AffiliateController->>AffiliateService: getAffiliateStatsByPartnerCode() / getAffiliateSwapsByPartnerCode()
AffiliateService->>PrismaService: findMany where partnerCode = ...
else address present
AffiliateController->>AffiliateService: getAffiliateStatsByAddress() / getAffiliateSwapsByAddress()
AffiliateService->>PrismaService: findMany where affiliate.walletAddress or receiveAddress = ...
else missing query
AffiliateController-->>Client: BadRequestException
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
The endpoint has no callers anywhere in ~/github/shapeshift (public-api proxies /v1/affiliate/* and /swaps/:quoteId only), so migrate it straight to the canonical partnerCode key rather than a backwards-compatible bridge. - route: /swaps/affiliate-fees/:partnerCode - service: calculateAffiliateFees -> calculateAffiliateFeesByPartnerCode, filtering directly on partnerCode Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
prisma/migrations/20260626000000_swap_partner_code_attribution/migration.sql (1)
38-45: 🩺 Stability & Availability | 🔵 TrivialVerify this migration is lock-safe for production.
ADD CONSTRAINTand non-concurrentCREATE INDEXcan block writes on a hotswapstable. If this runs against prod traffic, considerNOT VALID/VALIDATE CONSTRAINTandCREATE INDEX CONCURRENTLY, or document the maintenance window.🤖 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 `@prisma/migrations/20260626000000_swap_partner_code_attribution/migration.sql` around lines 38 - 45, The migration adds a foreign key and a standard index on the hot swaps table, which can block writes during deployment. Update the migration around the swaps_partnerCode_fkey and swaps_partnerCode_idx changes to use a lock-safe approach such as adding the constraint as NOT VALID then validating it separately, and creating the index with CONCURRENTLY if the database path supports it; otherwise, explicitly document that this migration must run in a maintenance window.apps/swap-service/src/affiliate/affiliate.service.ts (2)
110-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsolidate the duplicated stats/swaps query logic.
getAffiliateStatsByAddress/getAffiliateStatsByPartnerCodediffer only in thewherepartner filter yet repeat the identical aggregation loop, and the four methods all rebuild the samecreatedAtdate-range clause. Extracting a shared date-clause helper and a single aggregation routine that takes a partner-filter fragment would cut this duplication and prevent the two stats paths from diverging.🤖 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 `@apps/swap-service/src/affiliate/affiliate.service.ts` around lines 110 - 205, The affiliate query methods duplicate the same createdAt date-range filtering and, for stats, the same aggregation loop, which should be shared. Extract a helper for building the createdAt clause and a common stats aggregation routine used by getAffiliateStatsByAddress and getAffiliateStatsByPartnerCode, with the only varying input being the partner filter fragment. Keep getAffiliateSwapsByAddress and getAffiliateSwapsByPartnerCode aligned with the shared date helper so the query logic stays consistent.
110-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReuse the DTO types for the partner-code option params.
These methods inline
{ startDate?: Date; endDate?: Date; ... }while the address variants acceptAffiliateStatsQueryDto/AffiliateSwapsQueryDto. Reusing the DTO types keeps the option contracts consistent and avoids drift if the DTOs gain fields.Also applies to: 180-183
🤖 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 `@apps/swap-service/src/affiliate/affiliate.service.ts` around lines 110 - 113, Reuse the existing query DTOs for the partner-code stats methods instead of inlining option shapes: update getAffiliateStatsByPartnerCode and the matching partner-code swaps method to accept the same DTO types used by the address variants (AffiliateStatsQueryDto and AffiliateSwapsQueryDto). Keep the method contracts aligned by referencing those DTO symbols directly so any future DTO field changes apply consistently.
🤖 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 `@apps/swap-service/src/affiliate/affiliate.service.ts`:
- Around line 67-72: Address-based affiliate lookups are missing swaps that were
saved only with partnerAddress and no affiliate relation. Update
getAffiliateStatsByAddress and getAffiliateSwapsByAddress in
affiliate.service.ts to include a fallback filter on partnerAddress when
querying swaps, alongside the existing affiliate relation match, so legacy
migration-window rows remain reachable by address.
In `@apps/swap-service/src/swaps/swaps.service.ts`:
- Around line 155-173: The partner code lookup in swaps.service.ts should not
fall back to null on a Prisma query failure. In the branch inside the
partnerCode check of the swap resolution logic, keep returning null only when
findUnique returns no affiliate, but in the catch path for the prisma.affiliate
lookup, log the failure and rethrow so the create flow fails instead of silently
losing attribution. Use the existing logger.warn and the surrounding partnerCode
resolution block to locate the change.
---
Nitpick comments:
In `@apps/swap-service/src/affiliate/affiliate.service.ts`:
- Around line 110-205: The affiliate query methods duplicate the same createdAt
date-range filtering and, for stats, the same aggregation loop, which should be
shared. Extract a helper for building the createdAt clause and a common stats
aggregation routine used by getAffiliateStatsByAddress and
getAffiliateStatsByPartnerCode, with the only varying input being the partner
filter fragment. Keep getAffiliateSwapsByAddress and
getAffiliateSwapsByPartnerCode aligned with the shared date helper so the query
logic stays consistent.
- Around line 110-113: Reuse the existing query DTOs for the partner-code stats
methods instead of inlining option shapes: update getAffiliateStatsByPartnerCode
and the matching partner-code swaps method to accept the same DTO types used by
the address variants (AffiliateStatsQueryDto and AffiliateSwapsQueryDto). Keep
the method contracts aligned by referencing those DTO symbols directly so any
future DTO field changes apply consistently.
In
`@prisma/migrations/20260626000000_swap_partner_code_attribution/migration.sql`:
- Around line 38-45: The migration adds a foreign key and a standard index on
the hot swaps table, which can block writes during deployment. Update the
migration around the swaps_partnerCode_fkey and swaps_partnerCode_idx changes to
use a lock-safe approach such as adding the constraint as NOT VALID then
validating it separately, and creating the index with CONCURRENTLY if the
database path supports it; otherwise, explicitly document that this migration
must run in a maintenance window.
🪄 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: 1f947d77-1aff-40c0-a131-9b530827b476
📒 Files selected for processing (13)
apps/swap-service/src/affiliate/__tests__/affiliate.service.test.tsapps/swap-service/src/affiliate/affiliate.controller.tsapps/swap-service/src/affiliate/affiliate.service.tsapps/swap-service/src/affiliate/types.tsapps/swap-service/src/swaps/swaps.controller.tsapps/swap-service/src/swaps/swaps.service.tsapps/swap-service/src/verification/__tests__/fixtures/maya/swap.tsapps/swap-service/src/verification/__tests__/fixtures/near/swap.tsapps/swap-service/src/verification/__tests__/fixtures/relay/swap.tsapps/swap-service/src/verification/__tests__/fixtures/thorchain/swap.tsapps/swap-service/src/verification/swap-verification.service.tsprisma/migrations/20260626000000_swap_partner_code_attribution/migration.sqlprisma/schema/swap-service.prisma
💤 Files with no reviewable changes (1)
- apps/swap-service/src/verification/swap-verification.service.ts
A thrown affiliate lookup (operational/DB error) is distinct from an unknown code (findUnique returns null). Rethrow instead of swallowing, so creation fails loudly rather than silently persisting a partner swap with no attribution and losing payout data. Addresses CodeRabbit review on PR #44. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A thrown affiliate lookup (operational/DB error) is distinct from an unknown code (findUnique returns null). Rethrow instead of swallowing, so creation fails loudly rather than silently persisting a partner swap with no attribution and losing payout data. Addresses CodeRabbit review on PR #44. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0c8e910 to
1cbcd53
Compare
…Address Lets this branch deploy independently of the public-api change. When a caller sends a raw partnerAddress (no partnerCode) — as public-api does today — the write path reverse-resolves it to a partnerCode (unambiguous registry matches only) so swaps stay code-attributed and remain visible to the relation-keyed reads. public-api can switch to sending partnerCode later with no staging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Description
Swaps now attribute to partners by a stable
partnerCode(a real FK to the affiliate registry) instead of the mutablepartnerAddresspayout snapshot. This fixes two latent bugs in the old address-keyed attribution:receiveAddress, their swaps split across the old and new address, and no single address returns all of them.receiveAddressmixes partners —receiveAddressisn't unique, so address filtering could return two partners' swaps together.partnerCodeis unique, stable, and FK-enforced, so a partner sees all their swaps regardless of address history.What changed
Swap.partnerCodeadded as a real relation toAffiliate.partnerCode(nowNOT NULL);@@index([partnerCode]).partnerAddressis kept as a per-swap payout snapshot (for the legacy migration window and to leave the settlement payout policy — per-swap vs. current — open).20260626000000_swap_partner_code_attribution): backfillpartnerCodefrompartnerAddress(unambiguous registry matches only) → guard-abort if any affiliate lacks a code →SET NOT NULL→ add FK (ON DELETE SET NULL / ON UPDATE CASCADE) → add index. Additive;partnerAddressuntouched.resolvePartnerstamps a registry-resolvedpartnerCodeplus the affiliate's current payout address as a snapshot; unknown/absent codes fall back to a rawpartnerAddresswith null code (FK-safe).affiliaterelation /partnerCode, neverpartnerAddress. New code-keyedgetAffiliate{Stats,Swaps}ByPartnerCodefor settlement/reporting.GET /v1/affiliate/{swaps,stats}: dual-param — acceptpartnerCode(preferred) oraddressduring the migration window; 400 if neither.GET /swaps/affiliate-fees/:partnerCode: migrated straight topartnerCode(the endpoint has no callers anywhere in the org, so no bridge needed).partnerAddresspassthrough in the AVNU verifier stub (never used on-chain).Migration safety
The full migration was executed transactionally (and rolled back) against both prod and develop DBs — runs cleanly, FK creates against the
affiliates_partner_code_keyunique index, NOT-NULL guard doesn't fire (0 null-code affiliates in either env). Backfill attributes 9 swaps on prod / 38 on develop; junk/unmatched addresses safely land as nullpartnerCode. Nothing manual required beyondyarn db:migrate.Follow-ups (not in this PR)
partnerCode(it currently sendsaddressto/v1/affiliate/{stats,swaps}), then contract outaddress.actualAffiliateFeeAmountCryptoBaseUnit × partner rate), and the per-swap-vs-current payout-address decision./swaps/affiliate-feesis a USD reporting view, not the payout engine.checkSwapStatusnever inspects the source-tx receipt, so reverted/fabricated-hash swaps sit PENDING forever — should fail onreceipt.status === 0/ never-mined.Testing
yarn jestinapps/swap-service— 58/58 pass (added attribution-read guards: by-address filters via the relation, by-code filters directly, neither touchespartnerAddress).eslintclean on all changed files.prisma validatepasses; full migration dry-run verified transactionally against prod and develop (see Migration safety above).