CognxSafeTrack commited on
Commit ·
a3df350
1
Parent(s): 17a3e29
feat(admin): add Direct Setup button for super admins on orgs without phone number
Browse files- Add "Direct Setup" button (super admin only) on /clients for orgs without a WhatsApp number
- Previously the setup modal was only reachable via the Reconfigure button (orgs with a phone)
- Add i18n keys: clients.actions.direct_setup (en/fr/pt)
- Add scratch scripts for org configuration and DB queries
- Add doc: configurer-token-nouvelle-org.md — how to configure a Meta token on a new org
- apps/admin/src/locales/en.json +1 -0
- apps/admin/src/locales/fr.json +1 -0
- apps/admin/src/locales/pt.json +1 -0
- apps/admin/src/pages/ClientsManagementView.tsx +17 -6
- apps/api/scratch/connect_test4.ts +54 -0
- apps/api/scratch/connect_testcrm.ts +37 -0
- apps/api/scratch/list_orgs.ts +18 -0
- apps/api/scratch/register_prod_number.ts +30 -0
- docs/organisation-onboarding/configurer-token-nouvelle-org.md +192 -0
apps/admin/src/locales/en.json
CHANGED
|
@@ -651,6 +651,7 @@
|
|
| 651 |
"actions": {
|
| 652 |
"reconfigure_wa": "Reconfigure WhatsApp",
|
| 653 |
"connect_wa": "Connect WhatsApp (Meta)",
|
|
|
|
| 654 |
"personality_studio": "Personality Studio",
|
| 655 |
"ai_credits": "AI Credits",
|
| 656 |
"billing_details": "Details & Billing"
|
|
|
|
| 651 |
"actions": {
|
| 652 |
"reconfigure_wa": "Reconfigure WhatsApp",
|
| 653 |
"connect_wa": "Connect WhatsApp (Meta)",
|
| 654 |
+
"direct_setup": "Direct Setup",
|
| 655 |
"personality_studio": "Personality Studio",
|
| 656 |
"ai_credits": "AI Credits",
|
| 657 |
"billing_details": "Details & Billing"
|
apps/admin/src/locales/fr.json
CHANGED
|
@@ -651,6 +651,7 @@
|
|
| 651 |
"actions": {
|
| 652 |
"reconfigure_wa": "Reconfigurer WhatsApp",
|
| 653 |
"connect_wa": "Connecter WhatsApp (Meta)",
|
|
|
|
| 654 |
"personality_studio": "Personality Studio",
|
| 655 |
"ai_credits": "Crédits IA",
|
| 656 |
"billing_details": "Détails & Facturation"
|
|
|
|
| 651 |
"actions": {
|
| 652 |
"reconfigure_wa": "Reconfigurer WhatsApp",
|
| 653 |
"connect_wa": "Connecter WhatsApp (Meta)",
|
| 654 |
+
"direct_setup": "Configuration directe",
|
| 655 |
"personality_studio": "Personality Studio",
|
| 656 |
"ai_credits": "Crédits IA",
|
| 657 |
"billing_details": "Détails & Facturation"
|
apps/admin/src/locales/pt.json
CHANGED
|
@@ -651,6 +651,7 @@
|
|
| 651 |
"actions": {
|
| 652 |
"reconfigure_wa": "Reconfigurar WhatsApp",
|
| 653 |
"connect_wa": "Ligar WhatsApp (Meta)",
|
|
|
|
| 654 |
"personality_studio": "Personality Studio",
|
| 655 |
"ai_credits": "Créditos IA",
|
| 656 |
"billing_details": "Detalhes & Faturação"
|
|
|
|
| 651 |
"actions": {
|
| 652 |
"reconfigure_wa": "Reconfigurar WhatsApp",
|
| 653 |
"connect_wa": "Ligar WhatsApp (Meta)",
|
| 654 |
+
"direct_setup": "Configuração direta",
|
| 655 |
"personality_studio": "Personality Studio",
|
| 656 |
"ai_credits": "Créditos IA",
|
| 657 |
"billing_details": "Detalhes & Faturação"
|
apps/admin/src/pages/ClientsManagementView.tsx
CHANGED
|
@@ -247,12 +247,23 @@ export default function ClientsManagementView() {
|
|
| 247 |
)}
|
| 248 |
</div>
|
| 249 |
) : (
|
| 250 |
-
<
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
)}
|
| 257 |
<button
|
| 258 |
onClick={() => setSelectedOrgForPersonality(client)}
|
|
|
|
| 247 |
)}
|
| 248 |
</div>
|
| 249 |
) : (
|
| 250 |
+
<div className="flex gap-2">
|
| 251 |
+
<button
|
| 252 |
+
onClick={() => handleEmbeddedSignup(client.id)}
|
| 253 |
+
className="flex items-center gap-2 bg-indigo-600 text-white px-5 py-2.5 rounded-xl font-semibold hover:bg-indigo-700 transition shadow-lg shadow-indigo-100"
|
| 254 |
+
>
|
| 255 |
+
<MessageSquare className="w-4 h-4" /> {t('clients.actions.connect_wa')}
|
| 256 |
+
</button>
|
| 257 |
+
{isSuperAdmin && (
|
| 258 |
+
<button
|
| 259 |
+
onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: client.wabaId || '', metaBusinessId: '', accessToken: '', phoneNumberId: '' }); }}
|
| 260 |
+
className="flex items-center gap-2 bg-slate-700 text-white px-4 py-2.5 rounded-xl font-semibold hover:bg-slate-800 transition text-xs"
|
| 261 |
+
title={t('clients.actions.direct_setup')}
|
| 262 |
+
>
|
| 263 |
+
<ShieldCheck className="w-3.5 h-3.5" /> {t('clients.actions.direct_setup')}
|
| 264 |
+
</button>
|
| 265 |
+
)}
|
| 266 |
+
</div>
|
| 267 |
)}
|
| 268 |
<button
|
| 269 |
onClick={() => setSelectedOrgForPersonality(client)}
|
apps/api/scratch/connect_test4.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import 'dotenv/config';
|
| 2 |
+
import { PrismaClient } from '@prisma/client';
|
| 3 |
+
|
| 4 |
+
const prisma = new PrismaClient();
|
| 5 |
+
|
| 6 |
+
const TEST4_ORG_ID = '12247ec1-7629-4650-b695-237654793e0c';
|
| 7 |
+
const TESTCRM_ORG_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30';
|
| 8 |
+
const PHONE_NUMBER_ID = '1135406776315489';
|
| 9 |
+
const DISPLAY_PHONE = '+221788227676';
|
| 10 |
+
const WABA_ID = '1503271284790621';
|
| 11 |
+
|
| 12 |
+
async function main() {
|
| 13 |
+
// 1. Read testcrm's encrypted token (copy it as-is — same ENCRYPTION_SECRET)
|
| 14 |
+
const testcrm = await prisma.organization.findUnique({
|
| 15 |
+
where: { id: TESTCRM_ORG_ID },
|
| 16 |
+
select: { systemUserToken: true, wabaId: true }
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
if (!testcrm?.systemUserToken) {
|
| 20 |
+
throw new Error('testcrm has no systemUserToken — cannot proceed');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
console.log(`📋 Source : testcrm | waba: ${testcrm.wabaId} | token: ${testcrm.systemUserToken ? '✅' : '❌'}`);
|
| 24 |
+
|
| 25 |
+
// 2. Free WABA ID from testcrm first (unique constraint)
|
| 26 |
+
await prisma.organization.update({
|
| 27 |
+
where: { id: TESTCRM_ORG_ID },
|
| 28 |
+
data: { wabaId: null }
|
| 29 |
+
});
|
| 30 |
+
console.log(`✅ WABA ID retiré de testcrm`);
|
| 31 |
+
|
| 32 |
+
// 3. Copy WABA ID + encrypted token to test4
|
| 33 |
+
await prisma.organization.update({
|
| 34 |
+
where: { id: TEST4_ORG_ID },
|
| 35 |
+
data: {
|
| 36 |
+
wabaId: WABA_ID,
|
| 37 |
+
systemUserToken: testcrm.systemUserToken,
|
| 38 |
+
systemUserTokenIssuedAt: new Date()
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
console.log(`✅ WABA ID + token copiés vers test4 (WABA: ${WABA_ID})`);
|
| 42 |
+
|
| 43 |
+
// 4. Move the phone number from testcrm to test4
|
| 44 |
+
await prisma.whatsAppPhoneNumber.update({
|
| 45 |
+
where: { id: PHONE_NUMBER_ID },
|
| 46 |
+
data: { organizationId: TEST4_ORG_ID }
|
| 47 |
+
});
|
| 48 |
+
console.log(`✅ Numéro transféré vers test4 : ${DISPLAY_PHONE}`);
|
| 49 |
+
|
| 50 |
+
console.log('\n🎉 test4 est maintenant opérationnel avec le numéro', DISPLAY_PHONE);
|
| 51 |
+
await prisma.$disconnect();
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
main().catch(console.error);
|
apps/api/scratch/connect_testcrm.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import 'dotenv/config';
|
| 2 |
+
import { PrismaClient } from '@prisma/client';
|
| 3 |
+
|
| 4 |
+
const prisma = new PrismaClient();
|
| 5 |
+
|
| 6 |
+
async function main() {
|
| 7 |
+
const TESTCRM_ORG_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30';
|
| 8 |
+
const PHONE_NUMBER_ID = '1135406776315489';
|
| 9 |
+
const DISPLAY_PHONE = '+221 78 822 76 76';
|
| 10 |
+
const WABA_ID = '1503271284790621';
|
| 11 |
+
|
| 12 |
+
// 1. Retirer le numéro de default-org-id (où on l'avait mis par erreur)
|
| 13 |
+
await prisma.whatsAppPhoneNumber.update({
|
| 14 |
+
where: { id: PHONE_NUMBER_ID },
|
| 15 |
+
data: { organizationId: TESTCRM_ORG_ID }
|
| 16 |
+
});
|
| 17 |
+
console.log(`✅ Numéro transféré vers testcrm : ${DISPLAY_PHONE}`);
|
| 18 |
+
|
| 19 |
+
// 2. Mettre à jour le WABA ID sur testcrm
|
| 20 |
+
await prisma.organization.update({
|
| 21 |
+
where: { id: TESTCRM_ORG_ID },
|
| 22 |
+
data: { wabaId: WABA_ID }
|
| 23 |
+
});
|
| 24 |
+
console.log(`✅ WABA ID mis à jour sur testcrm : ${WABA_ID}`);
|
| 25 |
+
|
| 26 |
+
// 3. Retirer le WABA ID de default-org-id (il était incorrect)
|
| 27 |
+
await prisma.organization.update({
|
| 28 |
+
where: { id: 'default-org-id' },
|
| 29 |
+
data: { wabaId: null }
|
| 30 |
+
});
|
| 31 |
+
console.log(`✅ WABA ID retiré de default-org-id`);
|
| 32 |
+
|
| 33 |
+
console.log('\n🎉 testcrm est maintenant connecté au numéro +221 78 822 76 76');
|
| 34 |
+
await prisma.$disconnect();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
main().catch(console.error);
|
apps/api/scratch/list_orgs.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import 'dotenv/config';
|
| 2 |
+
import { PrismaClient } from '@prisma/client';
|
| 3 |
+
|
| 4 |
+
const prisma = new PrismaClient();
|
| 5 |
+
|
| 6 |
+
async function main() {
|
| 7 |
+
const orgs = await prisma.organization.findMany({
|
| 8 |
+
select: { id: true, name: true, slug: true, wabaId: true },
|
| 9 |
+
orderBy: { createdAt: 'desc' }
|
| 10 |
+
});
|
| 11 |
+
console.log('\n📋 Organisations disponibles :');
|
| 12 |
+
orgs.forEach(o => {
|
| 13 |
+
console.log(` - [${o.id}] ${o.name} (slug: ${o.slug || 'aucun'}, waba: ${o.wabaId || 'non configuré'})`);
|
| 14 |
+
});
|
| 15 |
+
await prisma.$disconnect();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
main().catch(console.error);
|
apps/api/scratch/register_prod_number.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import 'dotenv/config';
|
| 2 |
+
import { PrismaClient } from '@prisma/client';
|
| 3 |
+
|
| 4 |
+
const prisma = new PrismaClient();
|
| 5 |
+
|
| 6 |
+
async function main() {
|
| 7 |
+
const PHONE_NUMBER_ID = '1135406776315489';
|
| 8 |
+
const DISPLAY_PHONE = '+221 78 822 76 76';
|
| 9 |
+
const WABA_ID = '1503271284790621';
|
| 10 |
+
const ORG_ID = 'default-org-id';
|
| 11 |
+
|
| 12 |
+
// 1. Mise à jour du WABA ID sur l'organisation
|
| 13 |
+
await prisma.organization.update({
|
| 14 |
+
where: { id: ORG_ID },
|
| 15 |
+
data: { wabaId: WABA_ID }
|
| 16 |
+
});
|
| 17 |
+
console.log(`✅ WABA ID mis à jour : ${WABA_ID}`);
|
| 18 |
+
|
| 19 |
+
// 2. Enregistrement du numéro de production
|
| 20 |
+
await prisma.whatsAppPhoneNumber.upsert({
|
| 21 |
+
where: { id: PHONE_NUMBER_ID },
|
| 22 |
+
update: { displayPhone: DISPLAY_PHONE, organizationId: ORG_ID },
|
| 23 |
+
create: { id: PHONE_NUMBER_ID, displayPhone: DISPLAY_PHONE, organizationId: ORG_ID }
|
| 24 |
+
});
|
| 25 |
+
console.log(`✅ Numéro enregistré : ${DISPLAY_PHONE} (ID: ${PHONE_NUMBER_ID})`);
|
| 26 |
+
|
| 27 |
+
await prisma.$disconnect();
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
main().catch(console.error);
|
docs/organisation-onboarding/configurer-token-nouvelle-org.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configurer le token Meta sur une nouvelle organisation
|
| 2 |
+
|
| 3 |
+
**Public :** Super Admin XAMLÉ
|
| 4 |
+
**Durée :** 5 minutes
|
| 5 |
+
**Pré-requis :** Avoir un token Meta valide (`EAA...`) d'une org existante ou obtenu depuis Meta Business Suite
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Contexte
|
| 10 |
+
|
| 11 |
+
Quand une organisation est créée, elle n'a pas de connexion WhatsApp. Le dashboard affiche **"Configuration requise"**. Pour qu'elle devienne opérationnelle, il faut lui associer :
|
| 12 |
+
|
| 13 |
+
1. Un **WABA ID** — l'identifiant du compte WhatsApp Business Account Meta
|
| 14 |
+
2. Un **token d'accès** (`EAA...`) — long-lived token Meta pour appeler l'API Graph
|
| 15 |
+
3. Un **phoneNumberId** — l'ID du numéro WhatsApp (peut être fait plus tard si le numéro n'existe pas encore)
|
| 16 |
+
|
| 17 |
+
> **Important — contrainte unique sur wabaId :** Deux organisations ne peuvent pas avoir le même `wabaId` en même temps. Si tu veux que plusieurs orgs opèrent sous le même compte WABA (ex. plusieurs numéros), contacte un développeur pour retirer la contrainte `@unique` du schéma Prisma.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Méthode 1 — Via l'admin UI (recommandé, sans code)
|
| 22 |
+
|
| 23 |
+
Depuis **Mai 2026**, les super admins peuvent configurer directement le token depuis la page `/clients` sans passer par un script.
|
| 24 |
+
|
| 25 |
+
### Étapes
|
| 26 |
+
|
| 27 |
+
1. Aller sur `/clients` dans l'admin
|
| 28 |
+
2. Trouver l'organisation cible (affiche **"Configuration requise"**)
|
| 29 |
+
3. Cliquer sur le bouton **"Configuration directe"** (visible uniquement pour les super admins)
|
| 30 |
+
4. Dans le modal qui s'ouvre, remplir :
|
| 31 |
+
- **WABA ID** *(obligatoire)* — ex. `1503271284790621`
|
| 32 |
+
- **Token d'accès** *(obligatoire si le token n'est pas déjà stocké)* — coller le token `EAA...`
|
| 33 |
+
- **Phone Number ID** *(optionnel)* — laisser vide si le numéro n'est pas encore créé sur Meta
|
| 34 |
+
- **Meta Business ID** *(optionnel)*
|
| 35 |
+
5. Cliquer **"Enregistrer la configuration"**
|
| 36 |
+
|
| 37 |
+
> **Où trouver le WABA ID ?**
|
| 38 |
+
> [business.facebook.com/wa/manage/home](https://business.facebook.com/wa/manage/home) → Accounts → WhatsApp Accounts → l'ID numérique du compte
|
| 39 |
+
|
| 40 |
+
> **Où trouver le token ?**
|
| 41 |
+
> Meta for Developers → ton App → WhatsApp → Configuration → Générer un token
|
| 42 |
+
> OU copier depuis une org existante via le script `scratch/list_orgs.ts` (voir Méthode 2)
|
| 43 |
+
|
| 44 |
+
### Ce que ça fait en base
|
| 45 |
+
|
| 46 |
+
L'appel `POST /v1/organizations/:id/whatsapp-setup` :
|
| 47 |
+
- Chiffre et stocke le token dans `Organization.systemUserToken` (avec `ENCRYPTION_SECRET`)
|
| 48 |
+
- Enregistre le `wabaId` sur l'organisation
|
| 49 |
+
- Si `phoneNumberId` fourni : crée ou met à jour l'entrée `WhatsAppPhoneNumber` liée à l'org
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## Méthode 2 — Via script Prisma (quand l'UI ne suffit pas)
|
| 54 |
+
|
| 55 |
+
Utiliser cette méthode pour :
|
| 56 |
+
- Copier le token d'une org existante vers une nouvelle (même `ENCRYPTION_SECRET` → copie directe)
|
| 57 |
+
- Effectuer des opérations en masse
|
| 58 |
+
- Déboguer en production sans déploiement
|
| 59 |
+
|
| 60 |
+
### Pré-requis
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
# Les variables doivent être dans /Volumes/sms/Edtech/.env ou exportées
|
| 64 |
+
DATABASE_URL=...
|
| 65 |
+
ENCRYPTION_SECRET=...
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Étape 1 — Trouver les IDs des organisations
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
cd /Volumes/sms/Edtech/apps/api
|
| 72 |
+
npx tsx scratch/list_orgs.ts
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Sortie :
|
| 76 |
+
```
|
| 77 |
+
📋 Organisations disponibles :
|
| 78 |
+
- [12247ec1-...] test4 (slug: agro-sn, waba: non configuré)
|
| 79 |
+
- [ba012b65-...] testcrm (slug: test, waba: 1503271284790621)
|
| 80 |
+
- [default-org-id] XAMLÉ Global (slug: aucun, waba: 938685848818318)
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Étape 2 — Créer le script de configuration
|
| 84 |
+
|
| 85 |
+
Créer `apps/api/scratch/connect_<nom_org>.ts` :
|
| 86 |
+
|
| 87 |
+
```typescript
|
| 88 |
+
import 'dotenv/config';
|
| 89 |
+
import { PrismaClient } from '@prisma/client';
|
| 90 |
+
|
| 91 |
+
const prisma = new PrismaClient();
|
| 92 |
+
|
| 93 |
+
// IDs à remplir
|
| 94 |
+
const NOUVELLE_ORG_ID = 'xxx-xxx-xxx';
|
| 95 |
+
const SOURCE_ORG_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30'; // org qui a le token
|
| 96 |
+
const WABA_ID = '1503271284790621';
|
| 97 |
+
const PHONE_NUMBER_ID = ''; // laisser vide si pas encore de numéro
|
| 98 |
+
const DISPLAY_PHONE = '';
|
| 99 |
+
|
| 100 |
+
async function main() {
|
| 101 |
+
// 1. Lire le token chiffré depuis l'org source
|
| 102 |
+
const source = await prisma.organization.findUnique({
|
| 103 |
+
where: { id: SOURCE_ORG_ID },
|
| 104 |
+
select: { systemUserToken: true }
|
| 105 |
+
});
|
| 106 |
+
if (!source?.systemUserToken) throw new Error('Org source sans token');
|
| 107 |
+
|
| 108 |
+
// 2. Si WABA_ID est déjà utilisé par une autre org, la libérer d'abord
|
| 109 |
+
// await prisma.organization.update({ where: { id: SOURCE_ORG_ID }, data: { wabaId: null } });
|
| 110 |
+
|
| 111 |
+
// 3. Configurer la nouvelle org
|
| 112 |
+
await prisma.organization.update({
|
| 113 |
+
where: { id: NOUVELLE_ORG_ID },
|
| 114 |
+
data: {
|
| 115 |
+
wabaId: WABA_ID,
|
| 116 |
+
systemUserToken: source.systemUserToken, // copie directe — même clé de chiffrement
|
| 117 |
+
systemUserTokenIssuedAt: new Date()
|
| 118 |
+
}
|
| 119 |
+
});
|
| 120 |
+
console.log('✅ Token et WABA ID configurés');
|
| 121 |
+
|
| 122 |
+
// 4. Associer un numéro (optionnel)
|
| 123 |
+
if (PHONE_NUMBER_ID) {
|
| 124 |
+
await prisma.whatsAppPhoneNumber.upsert({
|
| 125 |
+
where: { id: PHONE_NUMBER_ID },
|
| 126 |
+
update: { displayPhone: DISPLAY_PHONE, organizationId: NOUVELLE_ORG_ID },
|
| 127 |
+
create: { id: PHONE_NUMBER_ID, displayPhone: DISPLAY_PHONE, organizationId: NOUVELLE_ORG_ID }
|
| 128 |
+
});
|
| 129 |
+
console.log('✅ Numéro associé :', DISPLAY_PHONE);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
await prisma.$disconnect();
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
main().catch(console.error);
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### Étape 3 — Exécuter
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
cd /Volumes/sms/Edtech/apps/api
|
| 142 |
+
npx tsx scratch/connect_<nom_org>.ts
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## Contrainte wabaId : pourquoi et quand la retirer
|
| 148 |
+
|
| 149 |
+
Le champ `Organization.wabaId` est `@unique` dans le schéma Prisma. Cela empêche deux orgs d'avoir le même WABA ID, ce qui est la règle aujourd'hui (1 org = 1 WABA = 1 numéro).
|
| 150 |
+
|
| 151 |
+
**Si à l'avenir** tu veux plusieurs orgs sous le même compte WABA (ex. entreprise avec plusieurs départements, chacun avec son propre numéro WhatsApp) :
|
| 152 |
+
|
| 153 |
+
1. Créer une migration Prisma qui retire `@unique` sur `wabaId` :
|
| 154 |
+
```prisma
|
| 155 |
+
wabaId String? // supprimer @unique
|
| 156 |
+
```
|
| 157 |
+
2. Appliquer : `pnpm --filter @repo/database migrate:dev`
|
| 158 |
+
3. Depuis lors, plusieurs orgs peuvent partager le même WABA ID
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Cas courants
|
| 163 |
+
|
| 164 |
+
### Nouvelle org — même WABA qu'une org existante (numéros différents)
|
| 165 |
+
→ Impossible actuellement sans retirer `@unique` (voir ci-dessus)
|
| 166 |
+
|
| 167 |
+
### Nouvelle org — WABA différent, token fourni par le client
|
| 168 |
+
→ Méthode 1 (admin UI) : remplir WABA ID + token dans le modal "Configuration directe"
|
| 169 |
+
|
| 170 |
+
### Nouvelle org — pas encore de numéro, token à copier depuis testcrm
|
| 171 |
+
→ Méthode 2 (script) avec `PHONE_NUMBER_ID` vide. L'org sera en "Configuration requise" jusqu'à l'ajout du numéro.
|
| 172 |
+
|
| 173 |
+
### Transférer un numéro d'une org à une autre
|
| 174 |
+
```typescript
|
| 175 |
+
await prisma.whatsAppPhoneNumber.update({
|
| 176 |
+
where: { id: PHONE_NUMBER_ID },
|
| 177 |
+
data: { organizationId: NOUVELLE_ORG_ID }
|
| 178 |
+
});
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## Organisations actuelles en base (Mai 2026)
|
| 184 |
+
|
| 185 |
+
| ID | Nom | WABA ID | Token | Numéro |
|
| 186 |
+
|----|-----|---------|-------|--------|
|
| 187 |
+
| `default-org-id` | XAMLÉ Global | `938685848818318` | ✅ `EAAURe...` | `969048009628694` |
|
| 188 |
+
| `ba012b65-...` | testcrm | `1503271284790621` | ✅ | `+221788227676` |
|
| 189 |
+
| `12247ec1-...` | test4 | null | ✅ (copié) | à configurer |
|
| 190 |
+
| `136f72d9-...` | test | null | — | — |
|
| 191 |
+
|
| 192 |
+
> Mettre ce tableau à jour après chaque onboarding.
|