Conversation
📝 WalkthroughWalkthroughThis PR introduces a comprehensive affiliate program management system, including CMS-backed repository, Dune export services, new API routes for affiliate registration and stats retrieval, periodic data export polling, and comprehensive validation schemas. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant API as API Server
participant CMS
participant DI as DI Container
participant Service as AffiliateStatsService
participant Dune as Dune Analytics
Client->>API: GET /affiliate/affiliate-stats/:address
API->>DI: resolve AffiliateStatsService
DI-->>API: return service instance
API->>Service: getAffiliateStats(address)
alt Cache Hit
Service-->>API: return cached result
else Cache Miss
Service->>Dune: executeQuery(query_id, address)
Dune-->>Service: return rows + metadata
Service->>Service: normalize & paginate results
Service->>Service: store in cache (TTL)
Service-->>API: return result
end
API-->>Client: 200 {stats}
sequenceDiagram
actor Client
participant API as API Server
participant CMS
participant DI as DI Container
participant Repo as AffiliatesRepository
participant Sig as Signature Verifier
participant Export as ExportService
participant Dune as Dune Analytics
Client->>API: POST /affiliate/:address (code, walletAddress, signature)
API->>API: validate address match & code format
API->>Sig: verify EIP-712 signature
alt Invalid Signature
API-->>Client: 401 Unauthorized
else Valid Signature
API->>DI: resolve AffiliatesRepository
DI-->>API: return CMS-backed repo
API->>Repo: check getAffiliateByCode(code)
Repo->>CMS: fetch affiliate by code
CMS-->>Repo: null or existing
alt Code Already Exists
API-->>Client: 409 Conflict
else Code Available
API->>Repo: createAffiliate(params)
Repo->>CMS: POST new affiliate
CMS-->>Repo: created affiliate
opt isDuneEnabled
API->>DI: resolve ExportService
DI-->>API: return export service
API->>Export: exportAffiliateProgramData()
Export->>Dune: uploadCsv(affiliate_program_data)
Dune-->>Export: { success: true }
end
API-->>Client: 201 {code, createdAt}
end
end
sequenceDiagram
participant Scheduler as Node Scheduler
participant Poller as affiliateProgramExportPoller
participant DI as DI Container
participant Service as ExportService
participant Repo as AffiliatesRepository
participant Dune as Dune Analytics
Scheduler->>Poller: start()
Poller->>Poller: immediate export (with lock)
Poller->>DI: resolve ExportService
DI-->>Poller: return service
Poller->>Service: exportAffiliateProgramDataIfChanged(lastSignature)
Service->>Repo: listAffiliates()
Repo-->>Service: all affiliates
Service->>Service: generate signature (rowCount, maxUpdatedAt)
alt Signature Changed
Service->>Service: build CSV from data
Service->>Dune: uploadCsv(affiliate_program_data)
Dune-->>Service: { success: true }
Service-->>Poller: { uploaded: true, result }
Poller->>Poller: update lastSignature
else No Change
Service-->>Poller: { uploaded: false, result }
end
Poller->>Poller: log result
Scheduler->>Poller: repeat in 5 minutes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
80a306d to
a08ad30
Compare
a34602a to
a172fef
Compare
libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts
Outdated
Show resolved
Hide resolved
libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts
Outdated
Show resolved
Hide resolved
96b6f5d to
1db4c25
Compare
1db4c25 to
abca9b4
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/api/src/app/inversify.config.ts (1)
146-163:⚠️ Potential issue | 🟡 MinorUse singleton scope to preserve AffiliateStatsService cache.
AffiliateStatsServiceImplkeeps an in-memory cache;toDynamicValueis transient by default and creates a new instance on each resolution. This service is fetched per request (in route handlers), defeating caching. Add.inSingletonScope()to the binding.♻️ Proposed fix
apiContainer .bind<AffiliateStatsService>(affiliateStatsServiceSymbol) .toDynamicValue( () => new AffiliateStatsServiceImpl(duneRepository, affiliateStatsCacheTtlMs) - ); + ) + .inSingletonScope();
🤖 Fix all issues with AI agents
In `@apps/api/src/app/routes/ref-codes/_code/index.ts`:
- Around line 18-28: The module currently resolves affiliatesRepository at
top-level which can throw when the binding is absent; move the
apiContainer.get(affiliatesRepositorySymbol) call into the refCodes Fastify
plugin body after the isCmsEnabled guard so resolution only happens when CMS is
enabled; specifically remove the top-level const affiliatesRepository assignment
and instead call apiContainer.get(affiliatesRepositorySymbol) inside refCodes
(after the early return) and use that local variable for subsequent logic.
In `@libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts`:
- Around line 97-110: createAffiliate stores raw code and walletAddress which
differs from the normalized values used by getAffiliateByWalletAddress and
getAffiliateByCode, causing unreachable records and duplicates; before calling
cmsPost in createAffiliate, normalize the inputs (e.g., trim and lowercase the
walletAddress and code using the same normalization logic/functions used by the
lookup methods) and persist those normalized values (keep signedMessage and
enabled handling intact) so stored records match lookup behavior.
In `@libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts`:
- Around line 129-147: The uploadCsv implementation in DuneRepositoryImpl is
passing a pre-stringified body to makeRequest which prevents makeRequest from
setting the JSON Content-Type header; update uploadCsv (function uploadCsv and
the call to this.makeRequest) to pass the payload object (not
JSON.stringify(payload)) so makeRequest can serialize and set headers, or
alternatively include a headers: {'Content-Type':'application/json'} in the
options if you must keep a stringified body; ensure the call to
this.makeRequest<UploadCsvResponse>('/uploads/csv', { method: 'POST', body: ...
}) sends a plain object body or explicit JSON header.
In
`@libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts`:
- Around line 116-149: The CSV builder in buildCsv and CSV_HEADERS omits the
AffiliateProgramRow.updated_at column, causing exported CSVs to drop that field;
update CSV_HEADERS to include 'updated_at' and add row.updated_at into the array
constructed inside buildCsv (ensuring it goes through csvEscape like the other
fields) so the header and row values stay aligned and the exported CSV contains
the updated_at values for each AffiliateProgramRow.
In `@libs/services/src/HooksService/HooksServiceImpl.spec.ts`:
- Around line 61-63: The MockDuneRepository.uploadCsv mock currently has no
parameters and thus does not match the DuneRepository.uploadCsv signature;
update the MockDuneRepository.uploadCsv method to accept the same parameters and
return type as the DuneRepository interface (match parameter names/types and the
Promise<{ success: boolean; message?: string }> return), so the class correctly
implements DuneRepository and compiles/tests pass.
🧹 Nitpick comments (1)
apps/api/src/main.ts (1)
4-17: Add poller teardown on server shutdown.Line 17: the poller likely installs an interval; without cleanup, shutdown/tests can leak timers. Consider returning a disposer from
startAffiliateProgramExportPollerand wiring it toserver.addHook('onClose', ...).♻️ Proposed change in this file
-startAffiliateProgramExportPoller(); +const stopAffiliateProgramExportPoller = startAffiliateProgramExportPoller(); +server.addHook('onClose', async () => { + await stopAffiliateProgramExportPoller?.(); +});Example update to the poller signature (separate file):
export function startAffiliateProgramExportPoller(): () => void { const intervalId = setInterval(run, POLL_INTERVAL_MS); run(); return () => clearInterval(intervalId); }
Summary
Implements
Depends on cowprotocol/cms#80
Changes
AffiliatesRepository(get by wallet/code, create, list)GET /affiliate/:addressPOST /affiliate/:address(EIP‑712 signed bind, chainId=1, code regex 5–20 A‑Z0‑9_-)GET /ref-codes/:codeGET /affiliate/affiliate-stats/:addressGET /affiliate/trader-stats/:addressuploadCsvAPI +AffiliateProgramExportService→table affiliate_program_dataTesting
In .env, set the following
Now you can do curl on the new endpoints
Summary by CodeRabbit
Release Notes