gestion des services

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-11-13 23:58:18 +00:00
parent 7262d03365
commit 4e359efd5e
10 changed files with 552 additions and 7 deletions

View File

@ -26,7 +26,7 @@ async function bootstrap() {
// Swagger // Swagger
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Payment Hub API') .setTitle('Merchant Config API')
.setDescription('Unified DCB Payment Aggregation Platform') .setDescription('Unified DCB Payment Aggregation Platform')
.setVersion('1.0.0') .setVersion('1.0.0')
.addBearerAuth() .addBearerAuth()

View File

@ -12,10 +12,10 @@ import {
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { MerchantService } from './services/merchant.service'; import { MerchantService } from '../services/merchant.service';
import { CreateMerchantPartnerDto } from './dto/create.merchant.dto'; import { CreateMerchantPartnerDto } from '../dto/create.merchant.dto';
import { UpdateMerchantPartnerDto } from './dto/ update.merchant.dto'; import { UpdateMerchantPartnerDto } from '../dto/ update.merchant.dto';
import { AddUserToMerchantDto, UpdateUserRoleDto } from './dto/merchant.user.dto'; import { AddUserToMerchantDto, UpdateUserRoleDto } from '../dto/merchant.user.dto';
@ApiTags('merchants') @ApiTags('merchants')
@Controller('merchants') @Controller('merchants')

View File

@ -0,0 +1,130 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import { ServiceManagementService } from '../services/service.service';
import { CreateServiceDto } from '../dto/create.service.dto';
import { UpdateServiceDto } from '../dto/update.service.dto';
import { CreatePlanDto } from '../dto/create.plan.dto';
import { UpdatePlanDto } from '../dto/update.plan.dto';
@ApiTags('services')
@Controller('services')
export class ServiceController {
constructor(private readonly serviceManagementService: ServiceManagementService) {}
// ==================== SERVICE ENDPOINTS ====================
@Post()
@ApiOperation({ summary: 'Create a new service for a merchant' })
@ApiResponse({ status: 201, description: 'Service created successfully' })
@ApiResponse({ status: 400, description: 'Bad request - validation failed' })
@ApiResponse({ status: 404, description: 'Merchant not found' })
create(@Body() createServiceDto: CreateServiceDto) {
return this.serviceManagementService.createService(createServiceDto);
}
@Get('merchant/:merchantId')
@ApiOperation({ summary: 'Get all services for a merchant' })
@ApiParam({ name: 'merchantId', type: Number })
@ApiResponse({ status: 200, description: 'List of merchant services' })
@ApiResponse({ status: 404, description: 'Merchant not found' })
findAllByMerchant(@Param('merchantId', ParseIntPipe) merchantId: number) {
return this.serviceManagementService.findAllByMerchant(merchantId);
}
@Get(':id')
@ApiOperation({ summary: 'Get service by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 200, description: 'Service found' })
@ApiResponse({ status: 404, description: 'Service not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.serviceManagementService.findOneService(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update service' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 200, description: 'Service updated successfully' })
@ApiResponse({ status: 404, description: 'Service not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateServiceDto: UpdateServiceDto,
) {
return this.serviceManagementService.updateService(id, updateServiceDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete service' })
@ApiParam({ name: 'id', type: Number })
@ApiResponse({ status: 204, description: 'Service deleted successfully' })
@ApiResponse({ status: 404, description: 'Service not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.serviceManagementService.removeService(id);
}
// ==================== PLAN ENDPOINTS ====================
@Post(':serviceId/plans')
@ApiOperation({ summary: 'Create a new plan for a service' })
@ApiParam({ name: 'serviceId', type: Number })
@ApiResponse({ status: 201, description: 'Plan created successfully' })
@ApiResponse({ status: 400, description: 'Bad request - validation failed' })
@ApiResponse({ status: 404, description: 'Service not found' })
createPlan(
@Param('serviceId', ParseIntPipe) serviceId: number,
@Body() createPlanDto: CreatePlanDto,
) {
return this.serviceManagementService.createPlan(serviceId, createPlanDto);
}
@Get(':serviceId/plans')
@ApiOperation({ summary: 'Get all plans for a service' })
@ApiParam({ name: 'serviceId', type: Number })
@ApiResponse({ status: 200, description: 'List of service plans' })
@ApiResponse({ status: 404, description: 'Service not found' })
findAllPlans(@Param('serviceId', ParseIntPipe) serviceId: number) {
return this.serviceManagementService.findAllPlansByService(serviceId);
}
@Get('plans/:planId')
@ApiOperation({ summary: 'Get plan by ID' })
@ApiParam({ name: 'planId', type: Number })
@ApiResponse({ status: 200, description: 'Plan found' })
@ApiResponse({ status: 404, description: 'Plan not found' })
findOnePlan(@Param('planId', ParseIntPipe) planId: number) {
return this.serviceManagementService.findOnePlan(planId);
}
@Patch('plans/:planId')
@ApiOperation({ summary: 'Update plan' })
@ApiParam({ name: 'planId', type: Number })
@ApiResponse({ status: 200, description: 'Plan updated successfully' })
@ApiResponse({ status: 404, description: 'Plan not found' })
updatePlan(
@Param('planId', ParseIntPipe) planId: number,
@Body() updatePlanDto: UpdatePlanDto,
) {
return this.serviceManagementService.updatePlan(planId, updatePlanDto);
}
@Delete('plans/:planId')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete plan' })
@ApiParam({ name: 'planId', type: Number })
@ApiResponse({ status: 204, description: 'Plan deleted successfully' })
@ApiResponse({ status: 404, description: 'Plan not found' })
removePlan(@Param('planId', ParseIntPipe) planId: number) {
return this.serviceManagementService.removePlan(planId);
}
}

View File

@ -0,0 +1,64 @@
import {
IsString,
IsNotEmpty,
IsEnum,
IsNumber,
Min,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Periodicity, Currency } from "generated/prisma";
export class CreatePlanDto {
@ApiProperty({
description: 'Plan name',
example: 'Monthly Premium',
})
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({
description: 'Plan type',
enum: Periodicity,
example: 'Monthly',
})
@IsEnum(Periodicity)
@IsNotEmpty()
type: Periodicity;
@ApiProperty({
description: 'Plan amount',
example: 5000,
})
@IsNumber()
@Min(0)
@IsNotEmpty()
amount: number;
@ApiProperty({
description: 'Tax amount',
example: 900,
})
@IsNumber()
@Min(0)
@IsNotEmpty()
tax: number;
@ApiProperty({
description: 'Currency',
enum: Currency,
example: 'XOF',
})
@IsEnum(Currency)
@IsNotEmpty()
currency: Currency;
@ApiProperty({
description: 'Billing periodicity',
enum: Periodicity,
example: 'Monthly',
})
@IsEnum(Periodicity)
@IsNotEmpty()
periodicity: Periodicity;
}

View File

@ -0,0 +1,28 @@
import { IsString, IsNotEmpty, IsOptional, IsInt } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateServiceDto {
@ApiProperty({
description: 'Service name',
example: 'Premium Streaming',
})
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({
description: 'Service description',
example: 'Access to premium content streaming',
})
@IsString()
@IsOptional()
description?: string;
@ApiProperty({
description: 'Merchant partner ID',
example: 1,
})
@IsInt()
@IsNotEmpty()
merchantPartnerId: number;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreatePlanDto } from './create.plan.dto';
export class UpdatePlanDto extends PartialType(CreatePlanDto) {}

View File

@ -0,0 +1,7 @@
import { PartialType } from '@nestjs/swagger';
import { CreateServiceDto } from './create.service.dto';
import { OmitType } from '@nestjs/swagger';
export class UpdateServiceDto extends PartialType(
OmitType(CreateServiceDto, ['merchantPartnerId'] as const),
) {}

View File

@ -0,0 +1,42 @@
import { Service, Plan, MerchantPartner, Periodicity, Currency } from "generated/prisma";
export type ServiceEntity = Service;
export type PlanEntity = Plan & {
service?: Service;
};
export type ServiceWithPlans = Service & {
plans: Plan[];
merchantPartner?: MerchantPartner;
};
export interface CreateServiceData {
name: string;
description?: string;
merchantPartnerId: number;
}
export interface UpdateServiceData {
name?: string;
description?: string;
}
export interface CreatePlanData {
name: string;
type: Periodicity;
amount: number;
tax: number;
currency: Currency;
periodicity: Periodicity;
serviceId: number;
}
export interface UpdatePlanData {
name?: string;
type?: Periodicity;
amount?: number;
tax?: number;
currency?: Currency;
periodicity?: Periodicity;
}

View File

@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { MerchantController } from './merchant.controller'; import { MerchantController } from './controllers/merchant.controller';
import { HttpUserServiceClient } from './services/user.service.client'; import { HttpUserServiceClient } from './services/user.service.client';
import { PrismaService } from 'src/shared/services/prisma.service'; import { PrismaService } from 'src/shared/services/prisma.service';
import { MerchantService } from './services/merchant.service'; import { MerchantService } from './services/merchant.service';
import { ServiceController } from './controllers/service.controller';
import { ServiceManagementService } from './services/service.service';
@Module({ @Module({
@ -15,9 +17,10 @@ import { MerchantService } from './services/merchant.service';
ConfigModule, ConfigModule,
EventEmitterModule.forRoot(), EventEmitterModule.forRoot(),
], ],
controllers: [MerchantController], controllers: [MerchantController,ServiceController],
providers: [ providers: [
MerchantService, MerchantService,
ServiceManagementService,
PrismaService, PrismaService,
HttpUserServiceClient HttpUserServiceClient
], ],

View File

@ -0,0 +1,267 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/shared/services/prisma.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { CreateServiceDto } from '../dto/create.service.dto';
import { UpdateServiceDto } from '../dto/update.service.dto';
import { CreatePlanDto } from '../dto/create.plan.dto';
import { UpdatePlanDto } from '../dto/update.plan.dto';
import { ServiceWithPlans, PlanEntity } from '../entities/service.entity';
@Injectable()
export class ServiceManagementService {
constructor(
private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2,
) {}
// ==================== SERVICE METHODS ====================
/**
* Create a new service for a merchant
*/
async createService(dto: CreateServiceDto): Promise<ServiceWithPlans> {
// Check if merchant exists
const merchant = await this.prisma.merchantPartner.findUnique({
where: { id: dto.merchantPartnerId },
});
if (!merchant) {
throw new NotFoundException(
`Merchant with ID ${dto.merchantPartnerId} not found`,
);
}
const service = await this.prisma.service.create({
data: {
name: dto.name,
description: dto.description,
merchantPartnerId: dto.merchantPartnerId,
},
include: {
plans: true,
merchantPartner: true,
},
});
this.eventEmitter.emit('service.created', {
serviceId: service.id,
serviceName: service.name,
merchantId: dto.merchantPartnerId,
timestamp: new Date(),
});
return service;
}
/**
* Find all services for a merchant
*/
async findAllByMerchant(merchantId: number): Promise<ServiceWithPlans[]> {
// Check if merchant exists
const merchant = await this.prisma.merchantPartner.findUnique({
where: { id: merchantId },
});
if (!merchant) {
throw new NotFoundException(`Merchant with ID ${merchantId} not found`);
}
return this.prisma.service.findMany({
where: { merchantPartnerId: merchantId },
include: {
plans: true,
merchantPartner: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
/**
* Find service by ID
*/
async findOneService(id: number): Promise<ServiceWithPlans> {
const service = await this.prisma.service.findUnique({
where: { id },
include: {
plans: true,
merchantPartner: true,
},
});
if (!service) {
throw new NotFoundException(`Service with ID ${id} not found`);
}
return service;
}
/**
* Update service
*/
async updateService(
id: number,
dto: UpdateServiceDto,
): Promise<ServiceWithPlans> {
await this.findOneService(id); // Check if exists
const service = await this.prisma.service.update({
where: { id },
data: {
name: dto.name,
description: dto.description,
},
include: {
plans: true,
merchantPartner: true,
},
});
this.eventEmitter.emit('service.updated', {
serviceId: id,
serviceName: service.name,
timestamp: new Date(),
});
return service;
}
/**
* Delete service
*/
async removeService(id: number): Promise<void> {
await this.findOneService(id); // Check if exists
await this.prisma.service.delete({
where: { id },
});
this.eventEmitter.emit('service.deleted', {
serviceId: id,
timestamp: new Date(),
});
}
// ==================== PLAN METHODS ====================
/**
* Create a new plan for a service
*/
async createPlan(
serviceId: number,
dto: CreatePlanDto,
): Promise<PlanEntity> {
// Check if service exists
await this.findOneService(serviceId);
const plan = await this.prisma.plan.create({
data: {
name: dto.name,
type: dto.type,
amount: dto.amount,
tax: dto.tax,
currency: dto.currency,
periodicity: dto.periodicity,
serviceId,
},
include: {
service: true,
},
});
this.eventEmitter.emit('plan.created', {
planId: plan.id,
planName: plan.name,
serviceId,
amount: plan.amount,
currency: plan.currency,
timestamp: new Date(),
});
return plan;
}
/**
* Find all plans for a service
*/
async findAllPlansByService(serviceId: number): Promise<PlanEntity[]> {
// Check if service exists
await this.findOneService(serviceId);
return this.prisma.plan.findMany({
where: { serviceId },
include: {
service: true,
},
orderBy: {
amount: 'asc',
},
});
}
/**
* Find plan by ID
*/
async findOnePlan(id: number): Promise<PlanEntity> {
const plan = await this.prisma.plan.findUnique({
where: { id },
include: {
service: true,
},
});
if (!plan) {
throw new NotFoundException(`Plan with ID ${id} not found`);
}
return plan;
}
/**
* Update plan
*/
async updatePlan(id: number, dto: UpdatePlanDto): Promise<PlanEntity> {
await this.findOnePlan(id); // Check if exists
const plan = await this.prisma.plan.update({
where: { id },
data: {
name: dto.name,
type: dto.type,
amount: dto.amount,
tax: dto.tax,
currency: dto.currency,
periodicity: dto.periodicity,
},
include: {
service: true,
},
});
this.eventEmitter.emit('plan.updated', {
planId: id,
planName: plan.name,
amount: plan.amount,
timestamp: new Date(),
});
return plan;
}
/**
* Delete plan
*/
async removePlan(id: number): Promise<void> {
await this.findOnePlan(id); // Check if exists
await this.prisma.plan.delete({
where: { id },
});
this.eventEmitter.emit('plan.deleted', {
planId: id,
timestamp: new Date(),
});
}
}