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
6 changes: 6 additions & 0 deletions BackendAcademy/src/rewards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,10 @@ export type {
LevelThreshold,
UserProgressionResponse,
ThresholdsResponse,
LeaderboardEntry,
LeaderboardResponse,
UserLeaderboardPosition,
PrizeDistribution,
PrizePoolResponse,
CreatePrizePoolRequest,
} from './interfaces/rewards.interfaces';
73 changes: 73 additions & 0 deletions BackendAcademy/src/rewards/interfaces/rewards.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,76 @@ export interface UserProgressionResponse {
export interface ThresholdsResponse {
thresholds: LevelThreshold[];
}

// ---------------------------------------------------------------------------
// Leaderboard types
// ---------------------------------------------------------------------------

/**
* A single entry on the leaderboard.
*/
export interface LeaderboardEntry {
/** Position (1-based) */
rank: number;
userId: string;
xp: number;
level: number;
/** Human-readable level title */
title: string;
}

/**
* Response shape for GET /rewards/leaderboard
*/
export interface LeaderboardResponse {
leaderboard: LeaderboardEntry[];
/** Total number of participants in the XP system */
totalParticipants: number;
}

/**
* Response shape for GET /rewards/leaderboard/:userId
*/
export interface UserLeaderboardPosition {
userId: string;
/** Current 1-based rank */
rank: number;
xp: number;
level: number;
title: string;
totalParticipants: number;
}

// ---------------------------------------------------------------------------
// Prize pool types
// ---------------------------------------------------------------------------

/**
* A single prize distribution to a user.
*/
export interface PrizeDistribution {
rank: number;
userId: string;
amount: number;
distributedAt: Date;
}

/**
* Response shape for prize pool endpoints.
*/
export interface PrizePoolResponse {
id: string;
totalAmount: number;
currency: string;
distributedAt: Date | null;
createdAt: Date;
distribution: PrizeDistribution[];
}

/**
* Body shape for POST /rewards/prize-pool
*/
export interface CreatePrizePoolRequest {
totalAmount: number;
currency?: string;
}
47 changes: 47 additions & 0 deletions BackendAcademy/src/rewards/rewards.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,50 @@ export function xpToNextLevel(xp: number, level: number): number {
if (level >= MAX_LEVEL) return 0;
return xpThresholdForLevel(level + 1) - xp;
}

// ---------------------------------------------------------------------------
// Leaderboard configuration
// ---------------------------------------------------------------------------

/**
* Default number of entries returned by the leaderboard endpoint
* when no explicit ?topN query parameter is provided.
*/
export const LEADERBOARD_DEFAULT_TOP_N = 100;

// ---------------------------------------------------------------------------
// Prize pool configuration
// ---------------------------------------------------------------------------

/**
* Default currency symbol used for prize pools.
*/
export const PRIZE_POOL_DEFAULT_CURRENCY = 'XLM';

/**
* Default prize pool amount (in whole currency units) when a pool
* is auto-created during distribution and no explicit amount was set.
*/
export const PRIZE_POOL_DEFAULT_AMOUNT = 1000;

/**
* Distribution schedule for the prize pool.
*
* Each entry defines what percentage of the total pool a user at
* a given rank receives. Ranks not listed receive no payout.
*
* Percentages should sum to 100 (or less — unallocated remainder
* stays in the pool for the next cycle).
*/
export const PRIZE_DISTRIBUTION_PERCENTAGES: { rank: number; percentage: number }[] = [
{ rank: 1, percentage: 30 },
{ rank: 2, percentage: 20 },
{ rank: 3, percentage: 15 },
{ rank: 4, percentage: 7.5 },
{ rank: 5, percentage: 7.5 },
{ rank: 6, percentage: 4 },
{ rank: 7, percentage: 4 },
{ rank: 8, percentage: 4 },
{ rank: 9, percentage: 4 },
{ rank: 10, percentage: 4 },
];
89 changes: 89 additions & 0 deletions BackendAcademy/src/rewards/rewards.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import {
Controller,
Get,
Post,
Param,
Query,
Body,
ParseIntPipe,
HttpCode,
HttpStatus,
NotFoundException,
} from '@nestjs/common';
import { RewardsService } from './rewards.service';
import type {
UserProgressionResponse,
ThresholdsResponse,
LevelThreshold,
LeaderboardResponse,
UserLeaderboardPosition,
PrizePoolResponse,
CreatePrizePoolRequest,
} from './interfaces/rewards.interfaces';

/**
Expand Down Expand Up @@ -82,4 +90,85 @@ export class RewardsController {
): UserProgressionResponse {
return this.rewardsService.getUserProgression(userId);
}

/**
* Returns the top N users on the leaderboard, sorted by XP descending.
*
* @param topN Number of entries (default 100, query parameter)
*
* @example
* GET /rewards/leaderboard?topN=10
* → { leaderboard: [{ rank: 1, userId: "…", xp: 5000, level: 8, title: "Apprentice 3" }, …] }
*/
@Get('leaderboard')
@HttpCode(HttpStatus.OK)
getLeaderboard(
@Query('topN', ParseIntPipe) topN: number,
): LeaderboardResponse {
return this.rewardsService.getLeaderboard(topN);
}

/**
* Returns a user's current rank, XP, level, and title.
*
* @example
* GET /rewards/leaderboard/abc-123
* → { userId: "abc-123", rank: 7, xp: 2000, level: 5, title: "Apprentice", totalParticipants: 42 }
*/
@Get('leaderboard/:userId')
@HttpCode(HttpStatus.OK)
getUserLeaderboardPosition(
@Param('userId') userId: string,
): UserLeaderboardPosition {
return this.rewardsService.getUserLeaderboardPosition(userId);
}

/**
* Returns the current (most recent) prize pool, or 404 if none exist.
*
* @example
* GET /rewards/prize-pool
* → { id: "prize_…", totalAmount: 1000, currency: "XLM", distributedAt: null, … }
*/
@Get('prize-pool')
@HttpCode(HttpStatus.OK)
getPrizePool(): PrizePoolResponse {
const pool = this.rewardsService.getPrizePool();
if (!pool) {
throw new NotFoundException('No prize pool has been created yet.');
}
return pool;
}

/**
* Creates a new prize pool with the given amount and optional currency.
*
* @example
* POST /rewards/prize-pool
* { "totalAmount": 5000, "currency": "XLM" }
* → { id: "prize_…", totalAmount: 5000, … }
*/
@Post('prize-pool')
@HttpCode(HttpStatus.CREATED)
createPrizePool(
@Body() body: CreatePrizePoolRequest,
): PrizePoolResponse {
return this.rewardsService.createPrizePool(
body.totalAmount,
body.currency,
);
}

/**
* Distributes the current prize pool to the top 10 leaderboard members.
*
* @example
* POST /rewards/prize-pool/distribute
* → { id: "prize_…", totalAmount: 1000, …, distributedAt: "…", distribution: […] }
*/
@Post('prize-pool/distribute')
@HttpCode(HttpStatus.OK)
distributePrizes(): PrizePoolResponse {
return this.rewardsService.distributePrizes();
}
}
Loading
Loading