gestion des services
This commit is contained in:
parent
7262d03365
commit
4e359efd5e
@ -26,7 +26,7 @@ async function bootstrap() {
|
||||
|
||||
// Swagger
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Payment Hub API')
|
||||
.setTitle('Merchant Config API')
|
||||
.setDescription('Unified DCB Payment Aggregation Platform')
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth()
|
||||
|
||||
@ -12,10 +12,10 @@ import {
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { MerchantService } from './services/merchant.service';
|
||||
import { CreateMerchantPartnerDto } from './dto/create.merchant.dto';
|
||||
import { UpdateMerchantPartnerDto } from './dto/ update.merchant.dto';
|
||||
import { AddUserToMerchantDto, UpdateUserRoleDto } from './dto/merchant.user.dto';
|
||||
import { MerchantService } from '../services/merchant.service';
|
||||
import { CreateMerchantPartnerDto } from '../dto/create.merchant.dto';
|
||||
import { UpdateMerchantPartnerDto } from '../dto/ update.merchant.dto';
|
||||
import { AddUserToMerchantDto, UpdateUserRoleDto } from '../dto/merchant.user.dto';
|
||||
|
||||
@ApiTags('merchants')
|
||||
@Controller('merchants')
|
||||
130
src/merchant/controllers/service.controller.ts
Normal file
130
src/merchant/controllers/service.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
64
src/merchant/dto/create.plan.dto.ts
Normal file
64
src/merchant/dto/create.plan.dto.ts
Normal 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;
|
||||
}
|
||||
28
src/merchant/dto/create.service.dto.ts
Normal file
28
src/merchant/dto/create.service.dto.ts
Normal 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;
|
||||
}
|
||||
4
src/merchant/dto/update.plan.dto.ts
Normal file
4
src/merchant/dto/update.plan.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreatePlanDto } from './create.plan.dto';
|
||||
|
||||
export class UpdatePlanDto extends PartialType(CreatePlanDto) {}
|
||||
7
src/merchant/dto/update.service.dto.ts
Normal file
7
src/merchant/dto/update.service.dto.ts
Normal 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),
|
||||
) {}
|
||||
42
src/merchant/entities/service.entity.ts
Normal file
42
src/merchant/entities/service.entity.ts
Normal 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;
|
||||
}
|
||||
@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
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 { PrismaService } from 'src/shared/services/prisma.service';
|
||||
import { MerchantService } from './services/merchant.service';
|
||||
import { ServiceController } from './controllers/service.controller';
|
||||
import { ServiceManagementService } from './services/service.service';
|
||||
|
||||
|
||||
@Module({
|
||||
@ -15,9 +17,10 @@ import { MerchantService } from './services/merchant.service';
|
||||
ConfigModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
],
|
||||
controllers: [MerchantController],
|
||||
controllers: [MerchantController,ServiceController],
|
||||
providers: [
|
||||
MerchantService,
|
||||
ServiceManagementService,
|
||||
PrismaService,
|
||||
HttpUserServiceClient
|
||||
],
|
||||
|
||||
267
src/merchant/services/service.service.ts
Normal file
267
src/merchant/services/service.service.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user