Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions BackendAcademy/src/rewards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ export { RewardsService } from './rewards.service';
export { RewardsController } from './rewards.controller';
export { StreakService } from './streak.service';
export { StreakController } from './streak.controller';
export { ReferralService } from './referral.service';
export { ReferralController } from './referral.controller';
export {
MAX_LEVEL,
levelForXp,
xpThresholdForLevel,
xpToNextLevel,
} from './rewards.constants';
export {
REFERRAL_BONUS_XLM,
REFERRAL_CURRENCY,
REFERRAL_EXPIRY_DAYS,
MAX_PENDING_REFERRALS_PER_USER,
} from './referral.constants';
export type {
LevelThreshold,
UserProgressionResponse,
Expand All @@ -25,3 +33,12 @@ export type {
CheckinResponse,
StreakRecord,
} from './interfaces/streak.interfaces';
export type {
ReferralRecord,
ReferralStatus,
ReferralSummaryResponse,
CreateReferralRequest,
QualifyReferralRequest,
PayReferralRequest,
ReferralUpdateResponse,
} from './interfaces/referral.interfaces';
107 changes: 107 additions & 0 deletions BackendAcademy/src/rewards/interfaces/referral.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Referral status options.
*
* - pending: Referee registered but has not yet met the qualifying condition
* (e.g. completing their first task / course).
* - qualified: Referee has met the qualifying condition; bonus is due.
* - paid: XLM bonus has been disbursed to the referrer's wallet.
* - expired: The referral window closed before the referee qualified.
*/
export type ReferralStatus = 'pending' | 'qualified' | 'paid' | 'expired';

/**
* A single referral record — one referrer → one referee link.
*/
export interface ReferralRecord {
/** Unique referral identifier */
id: string;
/** User ID of the person who invited the referee */
referrerId: string;
/** User ID of the person who was invited */
refereeId: string;
/** Current status of this referral */
status: ReferralStatus;
/** XLM bonus amount that will be (or has been) awarded to the referrer */
bonusAmount: number;
/** Currency of the bonus (always XLM in Phase 1) */
currency: string;
/** ISO 8601 timestamp when the referral was created */
createdAt: string;
/** ISO 8601 timestamp when the referee qualified (null if not yet qualified) */
qualifiedAt: string | null;
/** ISO 8601 timestamp when the bonus was paid out (null if not yet paid) */
paidAt: string | null;
}

/**
* Response shape for GET /rewards/referrals/:userId
*
* Returns summary stats plus a list of individual referral records
* for the given referrer.
*/
export interface ReferralSummaryResponse {
referrerId: string;
/** Total number of referrals made by this user (all statuses) */
totalReferrals: number;
/** Number of referrals that have been paid out */
paidReferrals: number;
/** Total XLM earned through referral bonuses (paid only) */
totalXlmEarned: number;
/** Pending XLM that is due once referees qualify */
pendingXlm: number;
/** All referral records for this referrer */
referrals: ReferralRecord[];
}

/**
* Request body for POST /rewards/referrals
*
* Called when a new user registers via a referral link.
*/
export interface CreateReferralRequest {
/** User ID of the referrer (the one who shared the link) */
referrerId: string;
/** User ID of the newly-registered referee */
refereeId: string;
/**
* Optional: override the default bonus amount for this referral.
* Useful for promotional campaigns. Falls back to REFERRAL_BONUS_XLM.
*/
bonusAmount?: number;
}

/**
* Request body for POST /rewards/referrals/:referralId/qualify
*
* Called by internal services when the referee completes the qualifying action
* (e.g. first task submission graded ≥ 70, first course completed, etc.).
*/
export interface QualifyReferralRequest {
/** Timestamp at which the qualifying event occurred */
qualifiedAt?: string;
}

/**
* Request body for POST /rewards/referrals/:referralId/pay
*
* Called by the payout service after the on-chain XLM transfer is confirmed.
*/
export interface PayReferralRequest {
/** ISO 8601 timestamp of the confirmed payout */
paidAt?: string;
}

/**
* Lightweight response returned after a state-changing operation
* (qualify / pay) to avoid re-fetching the full summary.
*/
export interface ReferralUpdateResponse {
referralId: string;
newStatus: ReferralStatus;
bonusAmount: number;
currency: string;
/** Set when transitioning to 'qualified' */
qualifiedAt: string | null;
/** Set when transitioning to 'paid' */
paidAt: string | null;
}
41 changes: 41 additions & 0 deletions BackendAcademy/src/rewards/referral.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Referral bonus configuration constants.
*
* All monetary values are expressed in whole XLM units.
* Replace hard-coded values with environment-variable reads
* (e.g. via ConfigService) when wiring up to production.
*/

/**
* Default XLM bonus credited to the referrer when their referee
* completes the qualifying action.
*/
export const REFERRAL_BONUS_XLM = 5;

/**
* Currency symbol for referral payouts (Phase 1 is XLM-only).
*/
export const REFERRAL_CURRENCY = 'XLM';

/**
* Number of days after which a pending referral is automatically
* marked as 'expired' if the referee has not qualified.
*
* Set to 0 to disable automatic expiry (useful for testing).
*/
export const REFERRAL_EXPIRY_DAYS = 30;

/**
* Maximum number of pending (unqualified) referrals a single user may
* hold at any one time. Prevents referral farming.
*
* Set to 0 to disable the cap.
*/
export const MAX_PENDING_REFERRALS_PER_USER = 50;

/**
* Maximum total referrals (all statuses) tracked per referrer.
* Older entries beyond this limit are not pruned automatically —
* this constant is intended for display / pagination defaults.
*/
export const REFERRAL_DISPLAY_LIMIT = 100;
121 changes: 121 additions & 0 deletions BackendAcademy/src/rewards/referral.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
Controller,
Get,
Post,
Param,
Body,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ReferralService } from './referral.service';
import type {
ReferralRecord,
ReferralSummaryResponse,
CreateReferralRequest,
QualifyReferralRequest,
PayReferralRequest,
ReferralUpdateResponse,
} from './interfaces/referral.interfaces';

/**
* ReferralController
*
* Placeholder REST interface for the referral-based XLM bonus system.
* All routes are prefixed with /rewards/referrals.
*
* Routes:
* POST /rewards/referrals – register a new referral
* GET /rewards/referrals/:userId – referrer's full summary
* GET /rewards/referrals/record/:referralId – single referral record
* POST /rewards/referrals/:referralId/qualify – mark referee as qualified
* POST /rewards/referrals/:referralId/pay – mark bonus as paid
*/
@Controller('rewards/referrals')
export class ReferralController {
constructor(private readonly referralService: ReferralService) {}

/**
* Registers a new referral when a user signs up via an invite link.
*
* @example
* POST /rewards/referrals
* { "referrerId": "user-abc", "refereeId": "user-xyz" }
* → 201 Created with the new ReferralRecord
*/
@Post()
@HttpCode(HttpStatus.CREATED)
createReferral(@Body() body: CreateReferralRequest): ReferralRecord {
return this.referralService.createReferral(
body.referrerId,
body.refereeId,
body.bonusAmount,
);
}

/**
* Returns a referrer's aggregated referral stats and full history.
*
* @example
* GET /rewards/referrals/user-abc
* → { referrerId, totalReferrals, paidReferrals, totalXlmEarned, … }
*/
@Get(':userId')
@HttpCode(HttpStatus.OK)
getReferralSummary(
@Param('userId') userId: string,
): ReferralSummaryResponse {
return this.referralService.getReferralSummary(userId);
}

/**
* Returns a single referral record by its unique ID.
*
* @example
* GET /rewards/referrals/record/ref_1234_abcde
* → { id, referrerId, refereeId, status, bonusAmount, … }
*/
@Get('record/:referralId')
@HttpCode(HttpStatus.OK)
getReferral(@Param('referralId') referralId: string): ReferralRecord {
return this.referralService.getReferral(referralId);
}

/**
* Transitions a referral from 'pending' → 'qualified'.
* Called by internal services when the referee completes their first
* qualifying action (e.g. first graded task, first completed course).
*
* @example
* POST /rewards/referrals/ref_1234_abcde/qualify
* → { referralId, newStatus: "qualified", bonusAmount: 5, … }
*/
@Post(':referralId/qualify')
@HttpCode(HttpStatus.OK)
qualifyReferral(
@Param('referralId') referralId: string,
@Body() body: QualifyReferralRequest,
): ReferralUpdateResponse {
const qualifiedAt = body.qualifiedAt
? new Date(body.qualifiedAt)
: undefined;
return this.referralService.qualifyReferral(referralId, qualifiedAt);
}

/**
* Transitions a referral from 'qualified' → 'paid'.
* Called by the payout service after the on-chain XLM transfer is confirmed.
*
* @example
* POST /rewards/referrals/ref_1234_abcde/pay
* → { referralId, newStatus: "paid", bonusAmount: 5, paidAt: "…", … }
*/
@Post(':referralId/pay')
@HttpCode(HttpStatus.OK)
payReferral(
@Param('referralId') referralId: string,
@Body() body: PayReferralRequest,
): ReferralUpdateResponse {
const paidAt = body.paidAt ? new Date(body.paidAt) : undefined;
return this.referralService.payReferral(referralId, paidAt);
}
}
Loading
Loading