feat: Manage Images using Minio Service

This commit is contained in:
diallolatoile 2026-01-13 03:49:19 +00:00
parent 33a9dbde36
commit 5e5ecb6cd1
3 changed files with 133 additions and 58 deletions

View File

@ -10,6 +10,8 @@ import {
ParseIntPipe, ParseIntPipe,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
DefaultValuePipe,
BadRequestException,
} 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';
@ -32,19 +34,23 @@ export class MerchantController {
@Get() @Get()
@ApiOperation({ summary: 'Get all merchants' }) @ApiOperation({ summary: 'Get all merchants' })
@ApiQuery({ name: 'skip', required: false, type: Number }) @ApiQuery({ name: 'skip', required: false, type: Number, example: 0 })
@ApiQuery({ name: 'take', required: false, type: Number }) @ApiQuery({ name: 'take', required: false, type: Number, example: 10 })
@ApiResponse({ status: 200, description: 'List of merchants' }) @ApiResponse({ status: 200, description: 'Paginated merchants' })
findAll( async findAll(
@Query('skip') skip?: string, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take') take?: string, @Query('take', new DefaultValuePipe(10), ParseIntPipe) take: number,
) { ) {
return this.merchantService.findAll(
skip ? parseInt(skip) : 0, if (skip < 0) throw new BadRequestException('skip must be >= 0');
take ? parseInt(take) : 10, if (take < 1 || take > 100) {
); throw new BadRequestException('take must be between 1 and 100');
} }
return this.merchantService.findAll(skip, take);
}
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get merchant by ID' }) @ApiOperation({ summary: 'Get merchant by ID' })
@ApiParam({ name: 'id', type: Number }) @ApiParam({ name: 'id', type: Number })

View File

@ -1,4 +1,4 @@
import { Injectable, NotFoundException, BadRequestException, ConflictException, Inject } from '@nestjs/common'; import { Injectable, NotFoundException, BadRequestException, ConflictException, Inject, InternalServerErrorException } from '@nestjs/common';
import { MerchantPartnerWithRelations, MerchantUserWithInfo } from '../entities/merchant.entity'; import { MerchantPartnerWithRelations, MerchantUserWithInfo } from '../entities/merchant.entity';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import type { UserServiceClient } from '../interfaces/user.service.interface'; import type { UserServiceClient } from '../interfaces/user.service.interface';
@ -97,23 +97,49 @@ export class MerchantService {
} }
/** /**
* Find all merchants with optional pagination * Find merchants with pagination and total count
*/ */
async findAll(skip = 0, take = 10): Promise<MerchantPartnerWithRelations[]> { async findAll(skip = 0, take = 10): Promise<{ items: MerchantPartnerWithRelations[], total: number }> {
if (skip < 0) throw new BadRequestException('skip must be >= 0');
if (take < 1 || take > 100) throw new BadRequestException('take must be between 1 et 100');
try {
const total = await this.prisma.merchantPartner.count();
// Ajuster le take si on dépasse le total
if (skip >= total) {
return { items: [], total };
}
const remaining = total - skip;
const adjustedTake = Math.min(take, remaining);
const merchants = await this.prisma.merchantPartner.findMany({ const merchants = await this.prisma.merchantPartner.findMany({
skip, skip,
take, take: adjustedTake,
include: { include: {
configs: true, configs: true,
merchantUsers: true, merchantUsers: true,
technicalContacts: true, technicalContacts: true,
}, },
orderBy: { orderBy: { createdAt: 'desc' }
createdAt: 'desc',
},
}); });
return Promise.all(merchants.map(m => this.enrichMerchantWithUserInfo(m))); const enrichedMerchants = await Promise.all(
merchants.map(async merchant => {
try {
return await this.enrichMerchantWithUserInfo(merchant);
} catch (e) {
console.error(`⚠️ Error enriching merchant ${merchant.id}:`, e);
return merchant;
}
})
);
return { items: enrichedMerchants, total };
} catch (error) {
console.error('❌ Failed to fetch merchants:', error);
throw new InternalServerErrorException('Failed to fetch merchants');
}
} }
/** /**
@ -186,17 +212,35 @@ export class MerchantService {
* Add user to merchant * Add user to merchant
*/ */
async addUserToMerchant(dto: AddUserToMerchantDto): Promise<MerchantUserWithInfo> { async addUserToMerchant(dto: AddUserToMerchantDto): Promise<MerchantUserWithInfo> {
// Check if merchant exists console.log('🔹 Starting addUserToMerchant process', dto);
await this.findOne(dto.merchantPartnerId);
// Validate user exists // 1⃣ Vérifier que le merchant existe
const userExists = await this.userServiceClient.verifyUserExists(dto.userId); let merchant;
try {
merchant = await this.findOne(dto.merchantPartnerId);
console.log(`✅ Merchant exists: ID=${dto.merchantPartnerId}`);
} catch (err) {
console.error(`❌ Merchant not found: ID=${dto.merchantPartnerId}`, err);
throw new BadRequestException(`Merchant with ID ${dto.merchantPartnerId} not found`);
}
// 2⃣ Vérifier que l'utilisateur existe dans le service utilisateur
let userExists: boolean;
try {
userExists = await this.userServiceClient.verifyUserExists(dto.userId);
console.log(`📌 User exists check for ID=${dto.userId}: ${userExists}`);
if (!userExists) { if (!userExists) {
throw new BadRequestException(`User with ID ${dto.userId} not found in user service`); throw new BadRequestException(`User with ID ${dto.userId} not found in user service`);
} }
} catch (err) {
console.error(`❌ Error verifying user existence: ID=${dto.userId}`, err);
throw err;
}
// Check if user already attached // 3⃣ Vérifier si l'utilisateur est déjà attaché au merchant
const existing = await this.prisma.merchantUser.findUnique({ let existing;
try {
existing = await this.prisma.merchantUser.findUnique({
where: { where: {
userId_merchantPartnerId: { userId_merchantPartnerId: {
userId: dto.userId, userId: dto.userId,
@ -204,30 +248,55 @@ export class MerchantService {
}, },
}, },
}); });
console.log(`📌 Existing association check:`, existing ? 'Exists' : 'Not exists');
if (existing) { if (existing) {
throw new ConflictException(`User ${dto.userId} is already attached to merchant ${dto.merchantPartnerId}`); throw new ConflictException(`User ${dto.userId} is already attached to merchant ${dto.merchantPartnerId}`);
} }
} catch (err) {
console.error(`❌ Error checking existing merchant user: ID=${dto.userId}`, err);
throw err;
}
// Create merchant user // 4⃣ Créer l'association
const merchantUser = await this.prisma.merchantUser.create({ let merchantUser: MerchantUserWithInfo;
try {
merchantUser = await this.prisma.merchantUser.create({
data: { data: {
userId: dto.userId, userId: dto.userId,
role: dto.role, role: dto.role,
merchantPartnerId: dto.merchantPartnerId, merchantPartnerId: dto.merchantPartnerId,
}, },
}); });
console.log(`✅ MerchantUser created: ID=${merchantUser.id}, UserID=${dto.userId}, MerchantID=${dto.merchantPartnerId}`);
} catch (err) {
console.error(`❌ Error creating merchantUser`, err);
throw new InternalServerErrorException('Failed to create merchant user');
}
// Emit event // 5⃣ Émettre l'événement
try {
this.eventEmitter.emit('merchant.user.attached', { this.eventEmitter.emit('merchant.user.attached', {
merchantId: dto.merchantPartnerId, merchantId: dto.merchantPartnerId,
userId: dto.userId, userId: dto.userId,
role: dto.role, role: dto.role,
timestamp: new Date(), timestamp: new Date(),
}); });
console.log(`📣 Event emitted: merchant.user.attached`);
} catch (err) {
console.warn(`⚠️ Failed to emit event for merchant.user.attached`, err);
}
// Enrich with user info // 6⃣ Récupérer les infos utilisateur enrichies
const userInfo = await this.userServiceClient.getUserInfo(dto.userId); let userInfo;
try {
userInfo = await this.userServiceClient.getUserInfo(dto.userId);
console.log(`📌 User info fetched for ID=${dto.userId}`);
} catch (err) {
console.error(`❌ Failed to fetch user info, returning merchantUser without enrichment`, err);
userInfo = null;
}
// 7⃣ Retourner l'objet final
return { return {
...merchantUser, ...merchantUser,
userInfo, userInfo,

View File

@ -148,7 +148,7 @@ export class HttpUserServiceClient implements UserServiceClient {
try { try {
const headers = await this.getAuthHeaders(); const headers = await this.getAuthHeaders();
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.get(`${this.baseUrl}/users/${userId}`, { headers }), this.httpService.get(`${this.baseUrl}/merchant-users/${userId}`, { headers }),
); );
return this.mapToUserInfo(response.data); return this.mapToUserInfo(response.data);
} catch (error) { } catch (error) {
@ -174,7 +174,7 @@ export class HttpUserServiceClient implements UserServiceClient {
try { try {
const headers = await this.getAuthHeaders(); const headers = await this.getAuthHeaders();
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.post(`${this.baseUrl}/users/batch`, { userIds }, { headers }), this.httpService.post(`${this.baseUrl}/merchant-users/batch`, { userIds }, { headers }),
); );
return response.data.map(user => this.mapToUserInfo(user)); return response.data.map(user => this.mapToUserInfo(user));
} catch (error) { } catch (error) {