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
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { AccessControlModule } from './access-control/access-control.module';
import { WaitlistModule } from './waitlist/waitlist.module';
import { EventsModule } from './events/events.module';
import { MembershipPlansModule } from './membership-plans/membership-plans.module';
import { CreditsModule } from './credits/credits.module';
import { TeamsModule } from './teams/teams.module';

@Module({
Expand Down Expand Up @@ -115,6 +116,7 @@ import { TeamsModule } from './teams/teams.module';
WaitlistModule,
EventsModule,
MembershipPlansModule,
CreditsModule,
TeamsModule,
],
controllers: [AppController],
Expand Down
64 changes: 64 additions & 0 deletions backend/src/credits/credits.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Controller,
Get,
Post,
Body,
Query,
ParseUUIDPipe,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { CreditsService } from './credits.service';
import { PurchaseCreditsDto } from './dto/purchase-credits.dto';
import { GetCurrentUser } from '../auth/decorators/getCurrentUser.decorator';

@ApiTags('Credits')
@ApiBearerAuth()
@Controller('credits')
export class CreditsController {
constructor(private readonly creditsService: CreditsService) {}

@Get('packs')
@ApiOperation({ summary: 'List active credit packs' })
async getPacks() {
const data = await this.creditsService.getCreditPacks();
return { message: 'Credit packs retrieved successfully', data };
}

@Post('purchase')
@ApiOperation({ summary: 'Purchase a credit pack' })
async purchase(
@Body() dto: PurchaseCreditsDto,
@GetCurrentUser('id') userId: string,
) {
const data = await this.creditsService.purchase(dto.creditPackId, userId);
return { message: 'Credit purchase initiated', data };
}

@Get('balance')
@ApiOperation({ summary: 'Get remaining credit hours' })
async getBalance(@GetCurrentUser('id') userId: string) {
const data = await this.creditsService.getBalance(userId);
return { message: 'Credit balance retrieved successfully', data };
}

@Get('history')
@ApiOperation({ summary: 'Get paginated credit transaction history' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getHistory(
@Query('page') page: string,
@Query('limit') limit: string,
@GetCurrentUser('id') userId: string,
) {
const data = await this.creditsService.getHistory(userId, {
page: page ? Number(page) : undefined,
limit: limit ? Number(limit) : undefined,
});
return { message: 'Credit history retrieved successfully', ...data };
}
}
37 changes: 37 additions & 0 deletions backend/src/credits/credits.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CreditPack } from './entities/credit-pack.entity';
import { UserCredit } from './entities/user-credit.entity';
import { UserCreditTransaction } from './entities/credit-transaction.entity';
import { Payment } from '../payments/entities/payment.entity';
import { User } from '../users/entities/user.entity';
import { CreditsService } from './credits.service';
import { CreditsController } from './credits.controller';
import { GetCreditPacksProvider } from './providers/get-credit-packs.provider';
import { PurchaseCreditsProvider } from './providers/purchase-credits.provider';
import { GetCreditBalanceProvider } from './providers/get-credit-balance.provider';
import { GetCreditHistoryProvider } from './providers/get-credit-history.provider';
import { PaystackProvider } from '../payments/providers/paystack.provider';

@Module({
imports: [
TypeOrmModule.forFeature([
CreditPack,
UserCredit,
UserCreditTransaction,
Payment,
User,
]),
],
controllers: [CreditsController],
providers: [
CreditsService,
GetCreditPacksProvider,
PurchaseCreditsProvider,
GetCreditBalanceProvider,
GetCreditHistoryProvider,
PaystackProvider,
],
exports: [CreditsService],
})
export class CreditsModule {}
44 changes: 44 additions & 0 deletions backend/src/credits/credits.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { GetCreditPacksProvider } from './providers/get-credit-packs.provider';
import { PurchaseCreditsProvider } from './providers/purchase-credits.provider';
import { GetCreditBalanceProvider } from './providers/get-credit-balance.provider';
import {
GetCreditHistoryProvider,
CreditHistoryQuery,
} from './providers/get-credit-history.provider';
import { CreditPack } from './entities/credit-pack.entity';
import { UserCreditTransaction } from './entities/credit-transaction.entity';

@Injectable()
export class CreditsService {
constructor(
private readonly getCreditPacksProvider: GetCreditPacksProvider,
private readonly purchaseCreditsProvider: PurchaseCreditsProvider,
private readonly getCreditBalanceProvider: GetCreditBalanceProvider,
private readonly getCreditHistoryProvider: GetCreditHistoryProvider,
) {}

getCreditPacks(): Promise<CreditPack[]> {
return this.getCreditPacksProvider.findAllActive();
}

purchase(creditPackId: string, userId: string) {
return this.purchaseCreditsProvider.purchase(creditPackId, userId);
}

getBalance(userId: string): Promise<{ remainingHours: number }> {
return this.getCreditBalanceProvider.getBalance(userId);
}

getHistory(
userId: string,
query: CreditHistoryQuery,
): Promise<{
data: UserCreditTransaction[];
total: number;
page: number;
limit: number;
}> {
return this.getCreditHistoryProvider.find(userId, query);
}
}
8 changes: 8 additions & 0 deletions backend/src/credits/dto/purchase-credits.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsUUID } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class PurchaseCreditsDto {
@ApiProperty({ description: 'The credit pack ID to purchase' })
@IsUUID()
creditPackId: string;
}
31 changes: 31 additions & 0 deletions backend/src/credits/entities/credit-pack.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';

@Entity('credit_packs')
export class CreditPack {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column({ type: 'decimal', precision: 8, scale: 2 })
hours: number;

@Column({ type: 'bigint' })
priceKobo: number;

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
35 changes: 35 additions & 0 deletions backend/src/credits/entities/credit-transaction.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
JoinColumn,
} from 'typeorm';
import { UserCredit } from './user-credit.entity';
import { CreditTransactionType } from '../enums/credit-transaction-type.enum';

@Entity('credit_transactions')
export class UserCreditTransaction {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('uuid')
userCreditId: string;

@ManyToOne(() => UserCredit)
@JoinColumn({ name: 'userCreditId' })
userCredit: UserCredit;

@Column({ type: 'enum', enum: CreditTransactionType })
type: CreditTransactionType;

@Column({ type: 'decimal', precision: 8, scale: 2 })
hours: number;

@Column({ type: 'text', nullable: true })
description: string | null;

@CreateDateColumn()
createdAt: Date;
}
21 changes: 21 additions & 0 deletions backend/src/credits/entities/user-credit.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
UpdateDateColumn,
} from 'typeorm';

@Entity('user_credits')
export class UserCredit {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
userId: string;

@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
remainingHours: number;

@UpdateDateColumn()
updatedAt: Date;
}
5 changes: 5 additions & 0 deletions backend/src/credits/enums/credit-transaction-type.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum CreditTransactionType {
PURCHASE = 'PURCHASE',
SPEND = 'SPEND',
REFUND = 'REFUND',
}
22 changes: 22 additions & 0 deletions backend/src/credits/providers/get-credit-balance.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserCredit } from '../entities/user-credit.entity';

@Injectable()
export class GetCreditBalanceProvider {
constructor(
@InjectRepository(UserCredit)
private readonly userCreditsRepository: Repository<UserCredit>,
) {}

async getBalance(userId: string): Promise<{ remainingHours: number }> {
const userCredit = await this.userCreditsRepository.findOne({
where: { userId },
});
if (!userCredit) {
return { remainingHours: 0 };
}
return { remainingHours: Number(userCredit.remainingHours) };
}
}
49 changes: 49 additions & 0 deletions backend/src/credits/providers/get-credit-history.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserCreditTransaction } from '../entities/credit-transaction.entity';
import { UserCredit } from '../entities/user-credit.entity';

export interface CreditHistoryQuery {
page?: number;
limit?: number;
}

@Injectable()
export class GetCreditHistoryProvider {
constructor(
@InjectRepository(UserCreditTransaction)
private readonly transactionsRepository: Repository<UserCreditTransaction>,
@InjectRepository(UserCredit)
private readonly userCreditsRepository: Repository<UserCredit>,
) {}

async find(
userId: string,
query: CreditHistoryQuery,
): Promise<{
data: UserCreditTransaction[];
total: number;
page: number;
limit: number;
}> {
const page = query.page ?? 1;
const limit = query.limit ?? 20;

const userCredit = await this.userCreditsRepository.findOne({
where: { userId },
});
if (!userCredit) {
return { data: [], total: 0, page, limit };
}

const [data, total] = await this.transactionsRepository.findAndCount({
where: { userCreditId: userCredit.id },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});

return { data, total, page, limit };
}
}
19 changes: 19 additions & 0 deletions backend/src/credits/providers/get-credit-packs.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreditPack } from '../entities/credit-pack.entity';

@Injectable()
export class GetCreditPacksProvider {
constructor(
@InjectRepository(CreditPack)
private readonly creditPacksRepository: Repository<CreditPack>,
) {}

async findAllActive(): Promise<CreditPack[]> {
return this.creditPacksRepository.find({
where: { isActive: true },
order: { createdAt: 'ASC' },
});
}
}
Loading
Loading