diff --git a/SEPA_EXPORT_AUDIT.md b/SEPA_EXPORT_AUDIT.md index aac49f4..bfd3876 100644 --- a/SEPA_EXPORT_AUDIT.md +++ b/SEPA_EXPORT_AUDIT.md @@ -97,9 +97,9 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements ## ⚠️ Limitations Connues ### 1. Support contrats -- ❌ **Seulement CDDU** : La requête SQL ne récupère que `cddu_contracts` -- ⚠️ **Pas de RG** : Les contrats régime général ne sont pas inclus -- **Impact** : Export incomplet pour organisations mixtes CDDU+RG +- ✅ **Tous types de contrats supportés** : CDDU et RG +- ℹ️ **Note technique** : La table `cddu_contracts` contient en réalité tous les types de contrats (nom historique) +- ✅ **Payslips** : Toutes les fiches de paie non payées sont incluses ### 2. Tests bancaires - ✅ **Qonto** : Testé et validé @@ -139,18 +139,18 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements - Montants corrects - Bénéficiaires corrects -### Phase 2 : Support Contrats RG -**Objectif** : Inclure les contrats régime général +### Phase 2 : Tests Élargis Supplémentaires (OPTIONNEL) +**Objectif** : Valider avec d'autres banques -**Modifications nécessaires** : -1. Modifier la requête SQL pour inclure `rg_contracts` -2. Unifier la structure de données (CDDU + RG) -3. Tester avec organisations mixtes -4. Mettre à jour la documentation +**Banques à tester** : +- [ ] BNP Paribas +- [ ] Crédit Agricole +- [ ] LCL +- [ ] Banque Postale -**Estimation** : 2-4 heures de développement +**Note** : Test Qonto réussi suffit pour activation prudente. Tests supplémentaires recommandés mais non bloquants. -### Phase 3 : Validation XSD (Optionnel) +### Phase 3 : Validation XSD (OPTIONNEL) **Objectif** : Valider le XML contre le schéma officiel **Avantages** : @@ -165,10 +165,10 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements ### Phase 4 : Activation Production **Pré-requis** : -- ✅ Tests réussis sur 3+ banques -- ✅ Support CDDU + RG -- ✅ Documentation utilisateur complète -- ✅ Support client prêt +- ✅ Tests réussis sur Qonto +- ✅ Support CDDU + RG (déjà fonctionnel) +- ⏳ Documentation utilisateur complète +- ⏳ Support client prêt **Actions** : 1. Réactiver le bouton d'export @@ -176,6 +176,8 @@ Le système d'export SEPA permet aux clients gérant eux-mêmes leurs virements 3. Communiquer la nouveauté aux clients 4. Monitorer les premiers exports +**Estimation activation** : Peut être fait rapidement, en attente de décision stratégique + --- ## 🐛 Problèmes Connus @@ -190,29 +192,34 @@ Les fonctionnalités testées fonctionnent correctement. Les limitations sont do ### Avant activation en production -1. **Tester avec plus de banques** (priorité haute) - - Demander à 2-3 clients volontaires de tester - - Vérifier compatibilité BNP, CA, SG - - Documenter les retours - -2. **Ajouter support RG** (priorité haute) - - Nécessaire pour organisations mixtes - - Évite confusion clients - -3. **Améliorer les messages d'erreur** (priorité moyenne) - - Erreurs plus explicites côté client - - Guide de résolution des problèmes - -4. **Documentation utilisateur** (priorité haute) +1. **Documentation utilisateur** (priorité haute) - Guide pas-à-pas - FAQ - Captures d'écran -5. **Monitoring** (priorité basse) +2. **Tester avec plus de banques** (priorité moyenne) + - Demander à 1-2 clients volontaires de tester + - Vérifier compatibilité BNP, CA, SG + - Documenter les retours + +3. **Améliorer les messages d'erreur** (priorité basse) + - Erreurs plus explicites côté client + - Guide de résolution des problèmes + +4. **Monitoring** (priorité basse) - Logger les exports réussis/échoués - Alertes en cas d'erreurs répétées - Analytics PostHog +### Note importante +Le système est **techniquement prêt pour activation** : +- ✅ Test réel réussi (Qonto) +- ✅ Tous types de contrats supportés +- ✅ Validation IBAN robuste +- ✅ Format SEPA conforme + +La désactivation actuelle est une **mesure de précaution** en attente de validation stratégique et documentation utilisateur. + ### Après activation 1. **Collecte de feedback** @@ -264,11 +271,13 @@ Les fonctionnalités testées fonctionnent correctement. Les limitations sont do ### Version 1.0 (3 novembre 2025) - Implémentation initiale - Test réussi avec Qonto -- Support CDDU uniquement +- Support de tous les types de contrats (CDDU + RG via table cddu_contracts) - Validation IBAN avec checksum modulo 97 - Interface de sélection multiple - Marquage groupé des paies -- **Statut** : Désactivé en production (en attente tests élargis) +- **Statut** : Désactivé en production par précaution +- **Raison désactivation** : En attente documentation utilisateur et validation stratégique +- **Prêt techniquement** : Oui ✓ --- diff --git a/app/(app)/virements-salaires/page.tsx b/app/(app)/virements-salaires/page.tsx index 7359f30..0e2d6a6 100644 --- a/app/(app)/virements-salaires/page.tsx +++ b/app/(app)/virements-salaires/page.tsx @@ -389,6 +389,8 @@ export default function VirementsPage() { const [isExporting, setIsExporting] = useState(false); const [isMarkingPaid, setIsMarkingPaid] = useState(false); const [showBulkMarkPaidModal, setShowBulkMarkPaidModal] = useState(false); + const [showChangeGestionModal, setShowChangeGestionModal] = useState(false); + const [isChangingGestion, setIsChangingGestion] = useState(false); const { data: userInfo, isLoading: isLoadingUser } = useUserInfo(); const { data: organizations, isLoading: isLoadingOrgs, error: orgsError } = useOrganizations(); @@ -704,6 +706,42 @@ export default function VirementsPage() { } } + // Changement de gestion des virements + async function handleChangeGestion() { + if (!org || !selectedOrgId) return; + setShowChangeGestionModal(false); + setIsChangingGestion(true); + + try { + const currentMode = isOdentas ? 'odentas' : 'client'; + const newMode = isOdentas ? 'client' : 'odentas'; + + const response = await fetch('/api/virements-salaires/change-gestion', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + organizationId: selectedOrgId, + newMode + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Erreur lors du changement de gestion'); + } + + // Recharger les données + await queryClient.invalidateQueries({ queryKey: ["virements-salaires"] }); + + } catch (error) { + console.error('Erreur changement gestion:', error); + alert(error instanceof Error ? error.message : 'Erreur lors du changement de gestion'); + } finally { + setIsChangingGestion(false); + } + } + // Filtrage local pour la recherche ET la période const filteredItems = useMemo((): VirementItem[] => { let result: VirementItem[] = items; @@ -873,23 +911,14 @@ export default function VirementsPage() {
Les virements de salaires sont effectués par
-
- -
- Bientôt disponible, veuillez nous contacter. -
-
-
+
@@ -1352,6 +1381,76 @@ export default function VirementsPage() {
)} + {/* Modal de changement de gestion des virements */} + {showChangeGestionModal && ( +
+
setShowChangeGestionModal(false)} + /> +
+
+
+
+
+ +
+

+ Changement de gestion des virements +

+
+
+
+ {isOdentas ? ( + <> +

+ Souhaitez-vous reprendre la gestion de vos virements de salaires ? +

+

+ Vous pourrez effectuer vous-même les virements via votre banque. +

+
+

+ Aucune modification tarifaire - Ce changement n'impacte pas votre facturation. +

+
+ + ) : ( + <> +

+ Souhaitez-vous confier la gestion de vos virements de salaires à Odentas ? +

+

+ Odentas s'occupera de redistribuer les salaires à vos salariés après réception de votre virement mensuel. +

+
+

+ Service sans surcoût - Ce changement n'impacte pas votre facturation. +

+
+ + )} +
+
+ + +
+
+
+
+ )} + {/* Modal PDF */} {pdfModalOpen && (
diff --git a/app/api/emails/signature-salarie/route.ts b/app/api/emails/signature-salarie/route.ts index 6c3d7b4..eab9d1c 100644 --- a/app/api/emails/signature-salarie/route.ts +++ b/app/api/emails/signature-salarie/route.ts @@ -74,48 +74,44 @@ export async function POST(request: NextRequest) { ); } - // 3. Récupération du vrai nom de l'organisation depuis Supabase - let organizationName = providedOrgName || 'Employeur'; + // 3. Récupération du nom du client depuis Supabase + let organizationName = 'Employeur'; // Fallback par défaut - if (organizationId || contractId) { - console.log('🔍 Récupération du nom de l\'organisation depuis la base de données...'); - try { - const supabase = createSbServiceRole(); - - let actualOrgId = organizationId; - - // Si on n'a que le contractId, récupérer l'org_id depuis le contrat - if (!actualOrgId && contractId) { - const { data: contractData } = await supabase - .from('cddu_contracts') - .select('org_id') - .eq('id', contractId) - .single(); - - if (contractData?.org_id) { - actualOrgId = contractData.org_id; - console.log('✅ org_id récupéré depuis le contrat:', actualOrgId); - } - } - - // Récupérer le nom de l'organisation - if (actualOrgId) { - const { data: orgData, error: orgError } = await supabase - .from('organizations') - .select('name') - .eq('id', actualOrgId) - .single(); - - if (!orgError && orgData?.name) { - organizationName = orgData.name; - console.log('✅ Nom de l\'organisation trouvé:', organizationName); - } else { - console.warn('⚠️ Nom de l\'organisation non trouvé, utilisation du fallback'); - } - } - } catch (err) { - console.error('⚠️ Erreur lors de la récupération du nom de l\'organisation:', err); + if (!organizationId) { + console.error('❌ organization_id manquant'); + return NextResponse.json( + { error: 'organization_id est requis' }, + { status: 400 } + ); + } + + console.log('🔍 Récupération du nom du client depuis organizations...'); + try { + const supabase = createSbServiceRole(); + + const { data: orgData, error: orgError } = await supabase + .from('organizations') + .select('name') + .eq('id', organizationId) + .single(); + + if (orgError) { + console.error('❌ Erreur lors de la récupération de l\'organisation:', orgError); + throw orgError; } + + if (orgData?.name) { + organizationName = orgData.name; + console.log('✅ Nom du client trouvé:', organizationName); + } else { + console.error('❌ Nom du client non trouvé pour org_id:', organizationId); + } + } catch (err) { + console.error('❌ Erreur lors de la récupération du nom du client:', err); + return NextResponse.json( + { error: 'Impossible de récupérer le nom du client' }, + { status: 500 } + ); } // 4. Récupération du prénom depuis Supabase si non fourni diff --git a/app/api/virements-salaires/change-gestion/route.ts b/app/api/virements-salaires/change-gestion/route.ts new file mode 100644 index 0000000..fa02c4b --- /dev/null +++ b/app/api/virements-salaires/change-gestion/route.ts @@ -0,0 +1,157 @@ +import { NextResponse } from "next/server"; +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; +export const runtime = 'nodejs'; + +export async function POST(req: Request) { + try { + const supabase = createRouteHandlerClient({ cookies }); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); + } + + const body = await req.json(); + const { organizationId, newMode } = body; + + if (!organizationId || !newMode) { + return NextResponse.json({ error: 'missing_parameters' }, { status: 400 }); + } + + if (!['odentas', 'client'].includes(newMode.toLowerCase())) { + return NextResponse.json({ error: 'invalid_mode' }, { status: 400 }); + } + + // Récupérer les informations de l'organisation + const { data: orgData, error: orgError } = await supabase + .from('organizations') + .select(` + id, + name, + structure_api, + organization_details( + org_id, + virements_salaires, + email_notifs, + email_notifs_cc, + code_employeur, + prenom_contact + ) + `) + .eq('id', organizationId) + .single(); + + if (orgError || !orgData) { + return NextResponse.json({ error: 'organization_not_found' }, { status: 404 }); + } + + const orgDetails = Array.isArray(orgData.organization_details) + ? orgData.organization_details[0] + : orgData.organization_details; + + const currentMode = orgDetails?.virements_salaires?.toLowerCase() || 'client'; + const targetMode = newMode.toLowerCase() === 'odentas' ? 'Odentas' : 'Client'; + + // Vérifier que le mode change réellement + if (currentMode === targetMode.toLowerCase()) { + return NextResponse.json({ + error: 'no_change_needed', + message: 'Le mode de gestion est déjà celui demandé' + }, { status: 400 }); + } + + // Mettre à jour le mode de gestion + const { error: updateError } = await supabase + .from('organization_details') + .update({ virements_salaires: targetMode }) + .eq('org_id', organizationId); + + if (updateError) { + console.error('Erreur mise à jour virements_salaires:', updateError); + return NextResponse.json({ error: 'update_failed' }, { status: 500 }); + } + + // Préparer les emails + const orgName = orgData.structure_api || orgData.name; + const emailClient = orgDetails?.email_notifs; + const emailClientCC = orgDetails?.email_notifs_cc; + + // Importer le service d'email + const { sendUniversalEmailV2 } = await import('@/lib/emailTemplateService'); + + // Email au client + if (emailClient) { + const emailType = targetMode === 'Odentas' + ? 'virements-gestion-to-odentas' + : 'virements-gestion-to-client'; + + const emailData = { + organizationName: orgName, + companyName: orgName, + employerCode: orgDetails?.code_employeur || '—', + handlerName: 'Renaud BREVIERE-ABRAHAM', + gestionMode: targetMode === 'Odentas' ? 'Géré par Odentas' : 'Géré en autonomie', + changeDate: new Date().toLocaleString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }), + firstName: orgDetails?.prenom_contact || '', + supportUrl: 'https://espace-paie.odentas.fr/support', + step1: 'Réception de l\'appel à virement mensuel', + step2: 'Virement unique vers Odentas', + step3: 'Redistribution automatique aux salariés', + }; + + try { + await sendUniversalEmailV2({ + type: emailType as any, + toEmail: emailClient, + ccEmail: emailClientCC || undefined, + data: emailData + }); + } catch (emailError) { + console.error('Erreur envoi email client:', emailError); + // Continue même si l'email échoue + } + } + + // Email interne à l'équipe Odentas + const internalData = { + organizationName: orgName, + previousMode: currentMode, + newMode: targetMode, + changeDate: new Date().toLocaleString('fr-FR'), + }; + + try { + await sendUniversalEmailV2({ + type: 'virements-gestion-internal', + toEmail: 'paie@odentas.fr', + data: internalData + }); + } catch (emailError) { + console.error('Erreur envoi email interne:', emailError); + // Continue même si l'email échoue + } + + return NextResponse.json({ + success: true, + previousMode: currentMode, + newMode: targetMode + }); + + } catch (error) { + console.error('Error changing gestion mode:', error); + return NextResponse.json({ + error: 'internal_server_error', + message: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }); + } +} diff --git a/app/api/virements-salaires/export-sepa/route.ts b/app/api/virements-salaires/export-sepa/route.ts index 1918f2b..fddc19c 100644 --- a/app/api/virements-salaires/export-sepa/route.ts +++ b/app/api/virements-salaires/export-sepa/route.ts @@ -302,6 +302,9 @@ export async function POST(req: Request) { xml += ' \n'; xml += ' SEPA\n'; xml += ' \n'; + xml += ' \n'; + xml += ' SALA\n'; + xml += ' \n'; xml += ' \n'; xml += ` ${requestedExecutionDate}\n`; xml += ' \n'; diff --git a/lib/emailTemplateService.ts b/lib/emailTemplateService.ts index edff1c0..ba77bd9 100644 --- a/lib/emailTemplateService.ts +++ b/lib/emailTemplateService.ts @@ -44,6 +44,9 @@ export type EmailTypeV2 = | 'contribution-notification' // Nouveau type pour notification de cotisations | 'production-declared' // Nouveau type pour notification de déclaration de production | 'sepa-mandate-request' // Nouveau type pour demande de signature de mandat SEPA + | 'virements-gestion-to-odentas' // Nouveau type pour changement gestion Client → Odentas + | 'virements-gestion-to-client' // Nouveau type pour changement gestion Odentas → Client + | 'virements-gestion-internal' // Nouveau type pour notification interne de changement de gestion | 'notification' // Support | 'support-reply' // Réponse du staff à un ticket support @@ -282,6 +285,94 @@ const EMAIL_TEMPLATES_V2: Record = { } }, + 'virements-gestion-to-odentas': { + subject: 'Modification de la gestion des virements de salaire – {{organizationName}}', + title: 'Modification de la gestion des virements de salaire', + greeting: 'Bonjour {{firstName}},', + mainMessage: 'Nous confirmons qu\'Odentas gère désormais vos virements de salaires.', + closingMessage: 'Chaque mois, nous vous envoyons un appel à virement avec le total des salaires nets. Vous effectuez un virement unique vers notre compte bancaire, et nous redistribuons les salaires à vos salariés.

L\'équipe Odentas vous remercie pour votre confiance.', + ctaText: 'Accès à l\'Espace Paie', + footerText: 'Vous recevez cet e-mail car vous êtes client de Odentas, pour vous notifier d\'une action sur votre compte.', + preheaderText: 'Modification de la gestion des virements · Votre compte', + colors: { + headerColor: STANDARD_COLORS.HEADER, + titleColor: '#0F172A', + buttonColor: STANDARD_COLORS.BUTTON, + buttonTextColor: STANDARD_COLORS.BUTTON_TEXT, + cardBackgroundColor: '#FFFFFF', + cardBorder: '#E5E7EB', + cardTitleColor: '#0F172A', + alertIndicatorColor: '#22C55E', + }, + ctaUrl: 'https://paie.odentas.fr', + infoCard: [ + { label: 'Votre structure', key: 'companyName' }, + { label: 'Votre code employeur', key: 'employerCode' }, + { label: 'Votre gestionnaire', key: 'handlerName' }, + ], + detailsCard: { + title: 'Comment ça fonctionne', + rows: [ + { label: 'Étape 1', key: 'step1' }, + { label: 'Étape 2', key: 'step2' }, + { label: 'Étape 3', key: 'step3' }, + ] + } + }, + + 'virements-gestion-to-client': { + subject: 'Modification de la gestion des virements de salaire – {{organizationName}}', + title: 'Modification de la gestion des virements de salaire', + greeting: 'Bonjour {{firstName}},', + mainMessage: 'Nous confirmons que vous gérez désormais vous-même vos virements de salaires.', + closingMessage: 'L\'équipe Odentas vous remercie pour votre confiance.', + ctaText: 'Accès à l\'Espace Paie', + footerText: 'Vous recevez cet e-mail car vous êtes client de Odentas, pour vous notifier d\'une action sur votre compte.', + preheaderText: 'Modification de la gestion des virements · Votre compte', + colors: { + headerColor: STANDARD_COLORS.HEADER, + titleColor: '#0F172A', + buttonColor: STANDARD_COLORS.BUTTON, + buttonTextColor: STANDARD_COLORS.BUTTON_TEXT, + cardBackgroundColor: '#FFFFFF', + cardBorder: '#E5E7EB', + cardTitleColor: '#0F172A', + alertIndicatorColor: '#3B82F6', + }, + ctaUrl: 'https://paie.odentas.fr', + infoCard: [ + { label: 'Votre structure', key: 'companyName' }, + { label: 'Votre code employeur', key: 'employerCode' }, + { label: 'Votre gestionnaire', key: 'handlerName' }, + ], + }, + + 'virements-gestion-internal': { + subject: '[Virements] Changement de gestion - {{organizationName}}', + title: 'Changement de gestion des virements', + greeting: 'Notification interne', + mainMessage: 'Un client a modifié son mode de gestion des virements salaires.', + closingMessage: 'Cette notification est automatique.', + footerText: 'Odentas - Notification interne', + preheaderText: 'Notification interne · Changement de gestion', + colors: { + headerColor: '#64748B', + titleColor: '#0F172A', + buttonColor: '#64748B', + buttonTextColor: '#FFFFFF', + cardBackgroundColor: '#FFFFFF', + cardBorder: '#E5E7EB', + cardTitleColor: '#0F172A', + alertIndicatorColor: '#F59E0B', + }, + infoCard: [ + { label: 'Organisation', key: 'organizationName' }, + { label: 'Mode précédent', key: 'previousMode' }, + { label: 'Nouveau mode', key: 'newMode' }, + { label: 'Date', key: 'changeDate' }, + ], + }, + 'referral': { subject: '{{referrer_first_name}} de {{organization_name}} vous recommande Odentas', title: 'Vous avez été recommandé',