From 37c685d73f0babb72160511b55199d1439a10aa0 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 7 Jun 2026 18:24:23 +0200 Subject: [PATCH] feat: request SIRET/SIREN admin approval from payment page --- .../controllers/organizations.controller.ts | 77 +++++++++- .../organizations/organizations.module.ts | 11 +- .../dashboard/booking/[id]/pay/page.tsx | 134 +++++++++++++++++- apps/frontend/src/lib/api/organizations.ts | 9 ++ 4 files changed, 219 insertions(+), 12 deletions(-) diff --git a/apps/backend/src/application/controllers/organizations.controller.ts b/apps/backend/src/application/controllers/organizations.controller.ts index cb4d752..f20eeb7 100644 --- a/apps/backend/src/application/controllers/organizations.controller.ts +++ b/apps/backend/src/application/controllers/organizations.controller.ts @@ -12,6 +12,7 @@ import { UsePipes, ValidationPipe, NotFoundException, + BadRequestException, ParseUUIDPipe, ParseIntPipe, DefaultValuePipe, @@ -41,10 +42,16 @@ import { ORGANIZATION_REPOSITORY, } from '@domain/ports/out/organization.repository'; import { Organization, OrganizationType } from '@domain/entities/organization.entity'; +import { + NotificationType, + NotificationPriority, +} from '@domain/entities/notification.entity'; +import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { RolesGuard } from '../guards/roles.guard'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { Roles } from '../decorators/roles.decorator'; +import { NotificationService } from '../services/notification.service'; import { v4 as uuidv4 } from 'uuid'; /** @@ -64,7 +71,9 @@ export class OrganizationsController { private readonly logger = new Logger(OrganizationsController.name); constructor( - @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository + @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository, + @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, + private readonly notificationService: NotificationService ) {} /** @@ -297,6 +306,72 @@ export class OrganizationsController { return OrganizationMapper.toDto(updatedOrg); } + /** + * Request SIRET/SIREN approval from admins + * + * Any authenticated user can call this to notify all admins + * that their organization's SIRET/SIREN needs approval. + */ + @Post('request-siret-approval') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Request SIRET/SIREN approval', + description: 'Sends a notification to all admins requesting manual SIRET/SIREN verification.', + }) + @ApiResponse({ status: 200, description: 'Approval request sent to admins' }) + @ApiResponse({ status: 400, description: 'No SIRET/SIREN registered or already verified' }) + async requestSiretApproval( + @CurrentUser() user: UserPayload + ): Promise<{ message: string }> { + const organization = await this.organizationRepository.findById(user.organizationId); + if (!organization) { + throw new NotFoundException('Organization not found'); + } + + if (!organization.siren && !organization.siret) { + throw new BadRequestException( + 'Aucun SIRET ou SIREN renseigné sur votre organisation. Veuillez les ajouter avant de demander la validation.' + ); + } + + if (organization.siretVerified) { + throw new BadRequestException('Votre SIRET/SIREN est déjà vérifié.'); + } + + const admins = await this.userRepository.findByRole('ADMIN'); + + const identifier = organization.siret + ? `SIRET ${organization.siret}` + : `SIREN ${organization.siren}`; + + await Promise.all( + admins.map(admin => + this.notificationService.createNotification({ + userId: admin.id, + organizationId: admin.organizationId, + type: NotificationType.ORGANIZATION_UPDATE, + priority: NotificationPriority.HIGH, + title: 'Demande de validation SIRET/SIREN', + message: `L'organisation "${organization.name}" demande la validation de son ${identifier}.`, + metadata: { + organizationId: organization.id, + organizationName: organization.name, + siret: organization.siret, + siren: organization.siren, + requestedBy: user.email, + }, + actionUrl: `/dashboard/admin/organizations`, + }) + ) + ); + + this.logger.log( + `[${user.email}] SIRET/SIREN approval requested for org ${organization.name} (${organization.id})` + ); + + return { message: 'Votre demande a été envoyée aux administrateurs.' }; + } + /** * List organizations * diff --git a/apps/backend/src/application/organizations/organizations.module.ts b/apps/backend/src/application/organizations/organizations.module.ts index 984bbee..8449035 100644 --- a/apps/backend/src/application/organizations/organizations.module.ts +++ b/apps/backend/src/application/organizations/organizations.module.ts @@ -1,15 +1,18 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { OrganizationsController } from '../controllers/organizations.controller'; +import { UsersModule } from '../users/users.module'; +import { NotificationsModule } from '../notifications/notifications.module'; -// Import domain ports import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository'; import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository'; import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity'; @Module({ imports: [ - TypeOrmModule.forFeature([OrganizationOrmEntity]), // 👈 This line registers the repository provider + TypeOrmModule.forFeature([OrganizationOrmEntity]), + UsersModule, + NotificationsModule, ], controllers: [OrganizationsController], providers: [ @@ -18,8 +21,6 @@ import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/ useClass: TypeOrmOrganizationRepository, }, ], - exports: [ - ORGANIZATION_REPOSITORY, // optional, if other modules need it - ], + exports: [ORGANIZATION_REPOSITORY], }) export class OrganizationsModule {} diff --git a/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx b/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx index 461b563..82edffb 100644 --- a/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx +++ b/apps/frontend/app/[locale]/dashboard/booking/[id]/pay/page.tsx @@ -19,8 +19,12 @@ import { CheckCircle, Copy, Clock, + ShieldCheck, + Send, } from 'lucide-react'; import { getCsvBooking, payBookingCommission, declareBankTransfer } from '@/lib/api/bookings'; +import { getOrganization, requestSiretApproval } from '@/lib/api/organizations'; +import { useAuth } from '@/lib/context/auth-context'; interface BookingData { id: string; @@ -42,6 +46,13 @@ interface BookingData { commissionAmountEur?: number; } +interface OrgData { + siren?: string | null; + siret?: string | null; + siretVerified?: boolean; + name: string; +} + type PaymentMethod = 'card' | 'transfer' | null; const BANK_DETAILS = { @@ -54,21 +65,32 @@ export default function PayCommissionPage() { const router = useRouter(); const params = useParams(); const bookingId = params.id as string; + const { user } = useAuth(); const [booking, setBooking] = useState(null); + const [org, setOrg] = useState(null); const [loading, setLoading] = useState(true); const [paying, setPaying] = useState(false); const [declaring, setDeclaring] = useState(false); const [error, setError] = useState(null); const [selectedMethod, setSelectedMethod] = useState(null); const [copied, setCopied] = useState(null); + const [requestingApproval, setRequestingApproval] = useState(false); + const [approvalSent, setApprovalSent] = useState(false); + const [approvalError, setApprovalError] = useState(null); useEffect(() => { - async function fetchBooking() { + async function fetchData() { try { - const data = await getCsvBooking(bookingId); - setBooking(data as any); - if (data.status !== 'PENDING_PAYMENT') { + const [bookingData, orgData] = await Promise.all([ + getCsvBooking(bookingId), + user?.organizationId ? getOrganization(user.organizationId) : Promise.resolve(null), + ]); + + setBooking(bookingData as any); + if (orgData) setOrg(orgData); + + if ((bookingData as any).status !== 'PENDING_PAYMENT') { router.replace('/dashboard/bookings'); } } catch (err) { @@ -77,8 +99,8 @@ export default function PayCommissionPage() { setLoading(false); } } - if (bookingId) fetchBooking(); - }, [bookingId, router]); + if (bookingId) fetchData(); + }, [bookingId, router, user?.organizationId]); const handlePayByCard = async () => { setPaying(true); @@ -104,6 +126,21 @@ export default function PayCommissionPage() { } }; + const handleRequestApproval = async () => { + setRequestingApproval(true); + setApprovalError(null); + try { + await requestSiretApproval(); + setApprovalSent(true); + } catch (err) { + setApprovalError( + err instanceof Error ? err.message : 'Erreur lors de l\'envoi de la demande' + ); + } finally { + setRequestingApproval(false); + } + }; + const copyToClipboard = (value: string, key: string) => { navigator.clipboard.writeText(value); setCopied(key); @@ -113,6 +150,14 @@ export default function PayCommissionPage() { const formatPrice = (price: number, currency: string) => new Intl.NumberFormat('fr-FR', { style: 'currency', currency }).format(price); + const hasSiretOrSiren = org && (org.siret || org.siren); + const siretNotVerified = hasSiretOrSiren && !org.siretVerified; + const identifier = org?.siret + ? `SIRET ${org.siret}` + : org?.siren + ? `SIREN ${org.siren}` + : null; + if (loading) { return (
@@ -164,6 +209,60 @@ export default function PayCommissionPage() { Finalisez votre booking en réglant la commission de service

+ {/* SIRET/SIREN non vérifié — bannière de demande */} + {siretNotVerified && ( +
+
+
+ +
+
+

+ Votre {identifier} n'est pas encore validé +

+

+ Un administrateur doit vérifier votre numéro d'identification avant de + finaliser certaines opérations. Vous pouvez lui envoyer une demande de validation + directement depuis cette page. +

+ + {approvalSent ? ( +
+ + + Demande envoyée aux administrateurs. Vous serez notifié dès que votre{' '} + {identifier} sera validé. + +
+ ) : ( +
+ {approvalError && ( +

{approvalError}

+ )} + +
+ )} +
+
+
+ )} + {error && (
@@ -420,6 +519,29 @@ export default function PayCommissionPage() {

+ {/* Organisation info */} + {org && ( +
+

{org.name}

+ {identifier && ( +
+ {identifier} + {org.siretVerified ? ( + + + Vérifié + + ) : ( + + + Non vérifié + + )} +
+ )} +
+ )} +

diff --git a/apps/frontend/src/lib/api/organizations.ts b/apps/frontend/src/lib/api/organizations.ts index 0e93533..3340b83 100644 --- a/apps/frontend/src/lib/api/organizations.ts +++ b/apps/frontend/src/lib/api/organizations.ts @@ -66,3 +66,12 @@ export async function updateOrganization( ): Promise { return patch(`/api/v1/organizations/${id}`, data); } + +/** + * Request SIRET/SIREN approval from admins + * POST /api/v1/organizations/request-siret-approval + * Requires: Authenticated user + */ +export async function requestSiretApproval(): Promise<{ message: string }> { + return post<{ message: string }>('/api/v1/organizations/request-siret-approval', {}); +}