Skip to content

fix: complete referral trigger flow and payout ownership enforcement#349

Merged
OlufunbiIK merged 1 commit intoOlufunbiIK:mainfrom
Kaylahray:feat/project-requirements
Mar 29, 2026
Merged

fix: complete referral trigger flow and payout ownership enforcement#349
OlufunbiIK merged 1 commit intoOlufunbiIK:mainfrom
Kaylahray:feat/project-requirements

Conversation

@Kaylahray
Copy link
Copy Markdown
Contributor

@Kaylahray Kaylahray commented Mar 28, 2026

Closes #326
Closes #327

Summary

This PR completes the referral and payout backend fixes.

#326 Referral flow

  • mounted the referral module
  • fixed authenticated user resolution in referral routes
  • connected reward claiming to the real tip.verified trigger
  • made reward claiming idempotent
  • added test coverage for success, self-referral, duplicate referral, and duplicate reward attempts

#327 Payout flow

  • mounted the payouts module
  • protected payout routes with auth
  • replaced the placeholder wallet check with real artist wallet ownership validation
  • enforced ownership on payout request, balance, history, status, and retry flows
  • added test coverage for threshold, ownership failure, and retry behavior

Summary by CodeRabbit

  • New Features

    • Integrated new referral module for managing referral codes and rewards.
    • Integrated new payouts module with artist payout functionality.
    • Implemented event-driven referral reward claiming triggered by verified tips.
  • Bug Fixes

    • Added user ownership validation to payout operations to prevent unauthorized access.
  • Security

    • Payouts endpoints now require JWT authentication.
    • Restricted payout asset types to XLM and USDC currencies.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

@Kaylahray is attempting to deploy a commit to the olufunbiik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

This PR mounts the ReferralModule and PayoutsModule in the application, enforces JWT authentication on both modules' controllers, adds artist/user ownership validation throughout the payout service, refactors the referral controller to use full CurrentUserData injection, and connects referral reward claiming to tip-verified events via an event listener.

Changes

Cohort / File(s) Summary
Module Registration
backend/src/app.module.ts
Registers ReferralModule and PayoutsModule in the root module's imports array, making referral and payout routes live.
Payout Type Tightening
backend/src/artiste-payout/create-payout.dto.ts, backend/src/artiste-payout/payout-request.entity.ts
Narrows assetCode property type from generic string to union literal `'XLM'
Payout Authentication & Ownership
backend/src/artiste-payout/payouts.controller.ts, backend/src/artiste-payout/payouts.service.ts, backend/src/artiste-payout/payouts.module.ts
Adds @UseGuards(JwtAuthGuard) and @ApiBearerAuth() to controller, updates all endpoints to accept authenticated user and extract userId, injects Artist repository in service, and implements assertArtistOwnership() helper enforcing user-to-artist mapping before balance/payout operations.
Payout Testing Updates
backend/src/artiste-payout/payouts.controller.spec.ts, backend/src/artiste-payout/payouts.service.spec.ts, backend/src/artiste-payout/payouts.integration.spec.ts
Refactors test suites to mock authenticated user context, pass userId as first argument to service methods, verify ownership-validation exceptions (ForbiddenException), and seed test data (Artist entity) in integration tests.
CurrentUser Decorator Enhancement
backend/src/auth/decorators/current-user.decorator.ts
Extends decorator to optionally extract specific fields (e.g., @CurrentUser('id') returns user.userId) or return full CurrentUserData object when no selector provided.
Referral Authentication & Ownership
backend/src/social-sharing/referral.controller.ts
Adds JWT auth guard requirement and updates method signatures to inject full CurrentUserData object instead of extracting id field; service calls now receive user.userId explicitly.
Referral Event-Driven Reward Claiming
backend/src/social-sharing/referral.service.ts
Modifies claimReward to return Promise<boolean> and accept optional triggerTipId for idempotency; adds @OnEvent("tip.verified") handler that automatically claims referral rewards when tips are verified, converting unique-constraint violations into ConflictException.
Referral Testing Updates
backend/src/social-sharing/referral.controller.spec.ts, backend/src/social-sharing/referral.service.spec.ts
Refactors controller and service test suites to pass authenticated user context, mock transaction-based referral lookups via query runner, adjust claimReward assertions for idempotency semantics, and add event-handler test coverage.
Tip Event Enhancement
backend/src/tips/events/tip-verified.event.ts
Adds senderUserId public read-only field to TipVerifiedEvent, exposing sender identity for downstream event handlers (e.g., referral reward claiming).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Authenticated Client
    participant ReferralCtrl as Referral Controller
    participant ReferralSvc as Referral Service
    participant TipsEventBus as Event Bus (Tip Service)
    participant ReferralListnr as Referral Event Listener

    Client->>ReferralCtrl: applyCode(currentUser, code)
    ReferralCtrl->>ReferralSvc: applyCode(user.userId, code)
    ReferralSvc->>ReferralSvc: Create referral record in transaction
    Note over ReferralSvc: Returns ApplyReferralResponseDto

    rect rgba(100, 200, 100, 0.5)
    Note over TipsEventBus,ReferralListnr: Separate Flow: Tip Verification Triggers Reward
    Client->>TipsEventBus: Verify tip
    TipsEventBus->>TipsEventBus: Emit tip.verified event
    TipsEventBus->>ReferralListnr: handleTipVerified(event)
    ReferralListnr->>ReferralSvc: claimReward(event.senderUserId, event.tipId)
    ReferralSvc->>ReferralSvc: Update referral reward claim status
    Note over ReferralSvc: Returns boolean (true if claimed, false if already claimed)
    end
Loading
sequenceDiagram
    participant Client as Authenticated Client
    participant PayoutCtrl as Payout Controller
    participant JwtGuard as JWT Auth Guard
    participant PayoutSvc as Payout Service
    participant ArtistRepo as Artist Repository

    Client->>PayoutCtrl: requestPayout(currentUser, dto)
    PayoutCtrl->>JwtGuard: Validate JWT & extract user
    JwtGuard->>PayoutCtrl: user.userId
    PayoutCtrl->>PayoutSvc: requestPayout(user.userId, dto)
    
    rect rgba(100, 150, 200, 0.5)
    Note over PayoutSvc,ArtistRepo: Ownership Validation
    PayoutSvc->>ArtistRepo: Load artist by artistId
    PayoutSvc->>PayoutSvc: assertArtistOwnership(user.userId, artist)
    alt Artist belongs to user
        PayoutSvc->>PayoutSvc: verifyArtistAddress(artist, destinationAddress)
        PayoutSvc->>PayoutSvc: Reserve balance & create pending payout
    else User does not own artist
        PayoutSvc->>PayoutCtrl: ForbiddenException
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through referrals and tips,
Where ownership guards each payout slip,
Events now spark rewards with a cheer,
Authentication keeps bad actors clear!
Module by module, the gates swing wide—
Stellar waves flow with user-scoped pride.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary changes: completing referral trigger flow and enforcing payout ownership checks as specified in the linked issues.
Linked Issues check ✅ Passed All coding requirements from #326 and #327 are met: referral module mounted, authenticated routes aligned, tip.verified trigger implemented, reward claiming idempotent; payout module mounted, routes authenticated, artist ownership validation enforced, balance/retry/history scoped to requesting artist.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issues: referral module mounting/auth/trigger, payout module mounting/auth/ownership enforcement, and supporting infrastructure (CurrentUser decorator, TipVerifiedEvent) are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
backend/src/tips/events/tip-verified.event.ts (1)

8-15: Consider consolidating sender identity fields.

You now expose both senderId and senderUserId with identical values. Optional cleanup: keep one canonical field and deprecate the alias to reduce downstream ambiguity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/tips/events/tip-verified.event.ts` around lines 8 - 15, The class
exposes duplicate sender identity fields (senderId and senderUserId); pick a
single canonical property (e.g., senderUserId) and remove the redundant one, or
preserve the old name only as a deprecated alias. Update the constructor
signature (constructor(..., senderUserId: string)) and assignments in the
constructor (assign to this.senderUserId only), and update any consumers of
TipVerifiedEvent, TipVerifiedEvent.constructor, or spreading of the event to use
the chosen canonical symbol; if you must keep the old name for compatibility,
keep senderId only as a getter or deprecated alias that returns
this.senderUserId and add a comment marking it deprecated.
backend/src/social-sharing/referral.service.spec.ts (1)

85-85: Consider removing ReferralController from service spec.

The controller is imported but not used in any test. This adds unnecessary coupling and could cause test failures if controller dependencies change.

♻️ Proposed fix
     const module: TestingModule = await Test.createTestingModule({
-      controllers: [ReferralController],
       providers: [
         ReferralService,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/social-sharing/referral.service.spec.ts` at line 85, Remove the
unused ReferralController from the test module in the referral service unit
tests: locate the TestingModule setup in referral.service.spec.ts that includes
controllers: [ReferralController] and delete the controllers entry and any
unused import of ReferralController so the spec only instantiates
ReferralService (and its required providers/mocks); this reduces unnecessary
coupling and prevents controller dependency breakage.
backend/src/artiste-payout/payouts.service.ts (1)

306-313: Consider returning the updated payout directly from the update result.

The current implementation calls findOne after update to return the updated entity. While functional, this creates a potential race condition if another process modifies the payout between update and read.

♻️ Alternative using QueryBuilder with RETURNING clause
-    await this.payoutRepo.update(payoutId, {
+    const result = await this.payoutRepo
+      .createQueryBuilder()
+      .update(PayoutRequest)
+      .set({
-      status: PayoutStatus.PENDING,
-      failureReason: null,
-      stellarTxHash: null,
-      processedAt: null,
-    });
+        status: PayoutStatus.PENDING,
+        failureReason: null,
+        stellarTxHash: null,
+        processedAt: null,
+      })
+      .where("id = :payoutId", { payoutId })
+      .returning("*")
+      .execute();

-    return this.payoutRepo.findOne({ where: { id: payoutId } });
+    return result.raw[0] as PayoutRequest;

Note: TypeORM's RETURNING support varies by database. Verify PostgreSQL is used in production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/artiste-payout/payouts.service.ts` around lines 306 - 313, The
current flow updates a payout via this.payoutRepo.update(...) then re-reads with
this.payoutRepo.findOne(...), risking a race; change to perform the update and
return the updated row in one operation (use the repository or QueryBuilder
update with .set({...}) .where({ id: payoutId }) and .returning('*') then
.execute()) and extract the returned row (from result.raw or
result.generatedMaps) to return directly; keep the same fields being set
(status: PayoutStatus.PENDING, failureReason: null, stellarTxHash: null,
processedAt: null) and ensure you verify PostgreSQL/RETURNING support in
production before merging.
backend/src/artiste-payout/payouts.service.spec.ts (1)

1-297: Consider adding test coverage for settlement lifecycle methods.

The spec covers user-facing operations but omits finaliseSuccess, finaliseFailure, getPendingPayouts, and markProcessing. These are critical for payout settlement integrity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/artiste-payout/payouts.service.spec.ts` around lines 1 - 297, Add
unit tests to payouts.service.spec.ts covering the settlement lifecycle methods
on PayoutsService: write specs for getPendingPayouts (should return pending
payouts via payoutRepo.find with correct filters), markProcessing (should set
status to PROCESSING and save/update the payout and create an audit),
finaliseSuccess (should set status to COMPLETED, set stellarTxHash and
processedAt, update balances/audit and commit transaction), and finaliseFailure
(should set status to FAILED, record failureReason, release reserved balance via
ArtistBalance update and create audit). In each test reference and mock
PayoutsService methods and repo interactions
(payoutRepo.find/findOne/update/save, balanceRepo.findOne/update,
auditRepo.save) and assert the expected repo calls, status transitions
(PayoutStatus.PROCESSING/COMPLETED/FAILED), and permission checks when
payout.artistId belongs to another artist (use artistRepo.findOne mocks to
simulate ownership).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/src/artiste-payout/payouts.service.spec.ts`:
- Around line 1-297: Add unit tests to payouts.service.spec.ts covering the
settlement lifecycle methods on PayoutsService: write specs for
getPendingPayouts (should return pending payouts via payoutRepo.find with
correct filters), markProcessing (should set status to PROCESSING and
save/update the payout and create an audit), finaliseSuccess (should set status
to COMPLETED, set stellarTxHash and processedAt, update balances/audit and
commit transaction), and finaliseFailure (should set status to FAILED, record
failureReason, release reserved balance via ArtistBalance update and create
audit). In each test reference and mock PayoutsService methods and repo
interactions (payoutRepo.find/findOne/update/save, balanceRepo.findOne/update,
auditRepo.save) and assert the expected repo calls, status transitions
(PayoutStatus.PROCESSING/COMPLETED/FAILED), and permission checks when
payout.artistId belongs to another artist (use artistRepo.findOne mocks to
simulate ownership).

In `@backend/src/artiste-payout/payouts.service.ts`:
- Around line 306-313: The current flow updates a payout via
this.payoutRepo.update(...) then re-reads with this.payoutRepo.findOne(...),
risking a race; change to perform the update and return the updated row in one
operation (use the repository or QueryBuilder update with .set({...}) .where({
id: payoutId }) and .returning('*') then .execute()) and extract the returned
row (from result.raw or result.generatedMaps) to return directly; keep the same
fields being set (status: PayoutStatus.PENDING, failureReason: null,
stellarTxHash: null, processedAt: null) and ensure you verify
PostgreSQL/RETURNING support in production before merging.

In `@backend/src/social-sharing/referral.service.spec.ts`:
- Line 85: Remove the unused ReferralController from the test module in the
referral service unit tests: locate the TestingModule setup in
referral.service.spec.ts that includes controllers: [ReferralController] and
delete the controllers entry and any unused import of ReferralController so the
spec only instantiates ReferralService (and its required providers/mocks); this
reduces unnecessary coupling and prevents controller dependency breakage.

In `@backend/src/tips/events/tip-verified.event.ts`:
- Around line 8-15: The class exposes duplicate sender identity fields (senderId
and senderUserId); pick a single canonical property (e.g., senderUserId) and
remove the redundant one, or preserve the old name only as a deprecated alias.
Update the constructor signature (constructor(..., senderUserId: string)) and
assignments in the constructor (assign to this.senderUserId only), and update
any consumers of TipVerifiedEvent, TipVerifiedEvent.constructor, or spreading of
the event to use the chosen canonical symbol; if you must keep the old name for
compatibility, keep senderId only as a getter or deprecated alias that returns
this.senderUserId and add a comment marking it deprecated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93689931-f0a7-4a35-a8d6-90f83e732cde

📥 Commits

Reviewing files that changed from the base of the PR and between 4b01495 and 69a2304.

📒 Files selected for processing (15)
  • backend/src/app.module.ts
  • backend/src/artiste-payout/create-payout.dto.ts
  • backend/src/artiste-payout/payout-request.entity.ts
  • backend/src/artiste-payout/payouts.controller.spec.ts
  • backend/src/artiste-payout/payouts.controller.ts
  • backend/src/artiste-payout/payouts.integration.spec.ts
  • backend/src/artiste-payout/payouts.module.ts
  • backend/src/artiste-payout/payouts.service.spec.ts
  • backend/src/artiste-payout/payouts.service.ts
  • backend/src/auth/decorators/current-user.decorator.ts
  • backend/src/social-sharing/referral.controller.spec.ts
  • backend/src/social-sharing/referral.controller.ts
  • backend/src/social-sharing/referral.service.spec.ts
  • backend/src/social-sharing/referral.service.ts
  • backend/src/tips/events/tip-verified.event.ts

@OlufunbiIK
Copy link
Copy Markdown
Owner

LGTM

@OlufunbiIK OlufunbiIK merged commit 56181a4 into OlufunbiIK:main Mar 29, 2026
1 of 3 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

2 participants