CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
70a5a84
·
1 Parent(s): bde43ec

feat(meta-status): store metaBusinessId for reliable business verification

Browse files

Add optional `metaBusinessId` field to Organization (schema + shared-types).
fetchMetaStatus now queries /{businessId}?fields=verification_status directly
when metaBusinessId is stored — bypasses WABA edge permission restrictions
that block test WABAs and certain system user token scopes.

Direct setup form gains a "Business ID Meta" input that saves via PUT /organizations/:id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/admin/src/pages/ClientsManagementView.tsx CHANGED
@@ -57,7 +57,7 @@ export default function ClientsManagementView() {
57
  const [billingOrg, setBillingOrg] = useState<Organization | null>(null);
58
  const [metaStatuses, setMetaStatuses] = useState<Record<string, MetaStatus>>({});
59
  const [setupOrg, setSetupOrg] = useState<Organization | null>(null);
60
- const [directSetup, setDirectSetup] = useState({ wabaId: '', accessToken: '', phoneNumberId: '' });
61
  const [isDirectSetupSaving, setIsDirectSetupSaving] = useState(false);
62
 
63
  const fetchMetaStatus = async (orgId: string, force = false) => {
@@ -99,9 +99,12 @@ export default function ClientsManagementView() {
99
  ...(directSetup.accessToken && { accessToken: directSetup.accessToken }),
100
  ...(directSetup.phoneNumberId && { phoneNumberId: directSetup.phoneNumberId }),
101
  }, token!);
 
 
 
102
  toast.success('Configuration WhatsApp mise à jour !');
103
  setSetupOrg(null);
104
- setDirectSetup({ wabaId: '', accessToken: '', phoneNumberId: '' });
105
  await fetchClients();
106
  setTimeout(() => fetchMetaStatus(setupOrg.id, true), 1000);
107
  } catch (err: any) {
@@ -204,7 +207,7 @@ export default function ClientsManagementView() {
204
  <div className="h-10 w-px bg-slate-100"></div>
205
  {metaStatuses[client.id] && !metaStatuses[client.id].loading && !metaStatuses[client.id].configured ? (
206
  <button
207
- onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: '', accessToken: '', phoneNumberId: client.phoneNumbers?.[0]?.id || '' }); }}
208
  className="flex items-center gap-2 bg-amber-500 text-white px-4 py-2 rounded-xl font-semibold hover:bg-amber-600 transition text-xs"
209
  >
210
  <AlertTriangle className="w-3.5 h-3.5" /> Reconfigurer WhatsApp
@@ -484,6 +487,19 @@ export default function ClientsManagementView() {
484
  Trouvez-le sur <a href="https://business.facebook.com/wa/manage/home" target="_blank" rel="noreferrer" className="text-indigo-500 underline">business.facebook.com → WhatsApp Manager</a>
485
  </p>
486
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  <div>
488
  <label className="block text-xs font-bold text-slate-500 mb-1">Token système <span className="text-slate-300">(optionnel — utilise le token env par défaut)</span></label>
489
  <input
 
57
  const [billingOrg, setBillingOrg] = useState<Organization | null>(null);
58
  const [metaStatuses, setMetaStatuses] = useState<Record<string, MetaStatus>>({});
59
  const [setupOrg, setSetupOrg] = useState<Organization | null>(null);
60
+ const [directSetup, setDirectSetup] = useState({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: '' });
61
  const [isDirectSetupSaving, setIsDirectSetupSaving] = useState(false);
62
 
63
  const fetchMetaStatus = async (orgId: string, force = false) => {
 
99
  ...(directSetup.accessToken && { accessToken: directSetup.accessToken }),
100
  ...(directSetup.phoneNumberId && { phoneNumberId: directSetup.phoneNumberId }),
101
  }, token!);
102
+ if (directSetup.metaBusinessId) {
103
+ await api.put(`/v1/organizations/${setupOrg.id}`, { metaBusinessId: directSetup.metaBusinessId }, token!);
104
+ }
105
  toast.success('Configuration WhatsApp mise à jour !');
106
  setSetupOrg(null);
107
+ setDirectSetup({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: '' });
108
  await fetchClients();
109
  setTimeout(() => fetchMetaStatus(setupOrg.id, true), 1000);
110
  } catch (err: any) {
 
207
  <div className="h-10 w-px bg-slate-100"></div>
208
  {metaStatuses[client.id] && !metaStatuses[client.id].loading && !metaStatuses[client.id].configured ? (
209
  <button
210
+ onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: client.phoneNumbers?.[0]?.id || '' }); }}
211
  className="flex items-center gap-2 bg-amber-500 text-white px-4 py-2 rounded-xl font-semibold hover:bg-amber-600 transition text-xs"
212
  >
213
  <AlertTriangle className="w-3.5 h-3.5" /> Reconfigurer WhatsApp
 
487
  Trouvez-le sur <a href="https://business.facebook.com/wa/manage/home" target="_blank" rel="noreferrer" className="text-indigo-500 underline">business.facebook.com → WhatsApp Manager</a>
488
  </p>
489
  </div>
490
+ <div>
491
+ <label className="block text-xs font-bold text-slate-500 mb-1">Business ID Meta <span className="text-slate-300">(optionnel — pour afficher la vérification)</span></label>
492
+ <input
493
+ type="text"
494
+ placeholder="ex: 25855038707486178"
495
+ value={directSetup.metaBusinessId}
496
+ onChange={e => setDirectSetup(s => ({ ...s, metaBusinessId: e.target.value }))}
497
+ className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm font-mono outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400"
498
+ />
499
+ <p className="text-[10px] text-slate-400 mt-1">
500
+ Visible sur <a href="https://business.facebook.com/settings/info" target="_blank" rel="noreferrer" className="text-indigo-500 underline">business.facebook.com → Paramètres → Informations</a>
501
+ </p>
502
+ </div>
503
  <div>
504
  <label className="block text-xs font-bold text-slate-500 mb-1">Token système <span className="text-slate-300">(optionnel — utilise le token env par défaut)</span></label>
505
  <input
apps/api/src/services/organization.ts CHANGED
@@ -133,26 +133,44 @@ export async function fetchMetaStatus(organizationId: string, forceRefresh = fal
133
  logger.error({ err, organizationId }, '[META-STATUS] WABA fetch failed');
134
  }
135
 
136
- // Call 2 — Business verification (optional, two attempts with different field paths)
137
  let businessId: string | undefined;
138
  let businessName: string | undefined;
139
  let businessVerified: boolean | undefined;
140
 
141
- // Attempt A: business edge (works on production WABAs with business_management scope)
142
- try {
143
- const bizRes = await fetch(
144
- `https://graph.facebook.com/v19.0/${org.wabaId}?fields=business{id,name,verification_status}`,
145
- { headers }
146
- );
147
- const bizData = await bizRes.json() as any;
148
- if (!bizData.error && bizData.business) {
149
- businessId = bizData.business.id;
150
- businessName = bizData.business.name;
151
- businessVerified = bizData.business.verification_status === 'verified';
152
- }
153
- } catch { /* non-fatal */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- // Attempt B: on_behalf_of_business_info fallback (broader token compatibility)
156
  if (businessId === undefined) {
157
  try {
158
  const obRes = await fetch(
@@ -163,7 +181,6 @@ export async function fetchMetaStatus(organizationId: string, forceRefresh = fal
163
  if (!obData.error && obData.on_behalf_of_business_info?.id) {
164
  businessId = obData.on_behalf_of_business_info.id;
165
  businessName = obData.on_behalf_of_business_info.name;
166
- // Fetch verification status directly from the business node
167
  const vRes = await fetch(
168
  `https://graph.facebook.com/v19.0/${businessId}?fields=name,verification_status`,
169
  { headers }
 
133
  logger.error({ err, organizationId }, '[META-STATUS] WABA fetch failed');
134
  }
135
 
136
+ // Call 2 — Business verification (optional, three attempts in priority order)
137
  let businessId: string | undefined;
138
  let businessName: string | undefined;
139
  let businessVerified: boolean | undefined;
140
 
141
+ // Attempt A: stored metaBusinessId most reliable, bypasses WABA edge permissions
142
+ if ((org as any).metaBusinessId) {
143
+ try {
144
+ const vRes = await fetch(
145
+ `https://graph.facebook.com/v19.0/${(org as any).metaBusinessId}?fields=name,verification_status`,
146
+ { headers }
147
+ );
148
+ const vData = await vRes.json() as any;
149
+ if (!vData.error) {
150
+ businessId = (org as any).metaBusinessId;
151
+ businessName = vData.name;
152
+ businessVerified = vData.verification_status === 'verified';
153
+ }
154
+ } catch { /* non-fatal */ }
155
+ }
156
+
157
+ // Attempt B: business edge on WABA (works on production WABAs with business_management scope)
158
+ if (businessId === undefined) {
159
+ try {
160
+ const bizRes = await fetch(
161
+ `https://graph.facebook.com/v19.0/${org.wabaId}?fields=business{id,name,verification_status}`,
162
+ { headers }
163
+ );
164
+ const bizData = await bizRes.json() as any;
165
+ if (!bizData.error && bizData.business) {
166
+ businessId = bizData.business.id;
167
+ businessName = bizData.business.name;
168
+ businessVerified = bizData.business.verification_status === 'verified';
169
+ }
170
+ } catch { /* non-fatal */ }
171
+ }
172
 
173
+ // Attempt C: on_behalf_of_business_info fallback (broader token compatibility)
174
  if (businessId === undefined) {
175
  try {
176
  const obRes = await fetch(
 
181
  if (!obData.error && obData.on_behalf_of_business_info?.id) {
182
  businessId = obData.on_behalf_of_business_info.id;
183
  businessName = obData.on_behalf_of_business_info.name;
 
184
  const vRes = await fetch(
185
  `https://graph.facebook.com/v19.0/${businessId}?fields=name,verification_status`,
186
  { headers }
packages/database/prisma/schema.prisma CHANGED
@@ -11,6 +11,7 @@ model Organization {
11
  id String @id @default(uuid())
12
  name String
13
  wabaId String? @unique
 
14
  systemUserToken String?
15
  createdAt DateTime @default(now())
16
  updatedAt DateTime @updatedAt
 
11
  id String @id @default(uuid())
12
  name String
13
  wabaId String? @unique
14
+ metaBusinessId String?
15
  systemUserToken String?
16
  createdAt DateTime @default(now())
17
  updatedAt DateTime @updatedAt
packages/shared-types/src/organization.ts CHANGED
@@ -38,6 +38,7 @@ export const OrganizationSchema = z.object({
38
  webhookSecret: z.string().optional(),
39
  knowledgeBaseUrl: z.string().url().optional().or(z.literal('')),
40
  systemUserToken: z.string().optional(),
 
41
  brandingData: z.any().optional(),
42
  });
43
 
 
38
  webhookSecret: z.string().optional(),
39
  knowledgeBaseUrl: z.string().url().optional().or(z.literal('')),
40
  systemUserToken: z.string().optional(),
41
+ metaBusinessId: z.string().optional(),
42
  brandingData: z.any().optional(),
43
  });
44