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

feat(meta-status): sync daily limit + quality rating live from Meta phone number API

Browse files

fetchMetaStatus now calls GET /{phoneNumberId}?fields=messaging_limit_tier,quality_rating
to get the real tier from Meta (TIER_1K, TIER_10K, TIER_100K, UNLIMITED) instead of
a hardcoded value inferred from WABA status.

Frontend: TIER_LABELS map for human-readable labels, QualityDot badge (green/amber/red)
displayed next to the limit. dailyLimitLabel now uses the real tier from the API.

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

apps/admin/src/pages/ClientsManagementView.tsx CHANGED
@@ -25,6 +25,8 @@ interface MetaStatus {
25
  businessId?: string;
26
  businessName?: string;
27
  businessVerified?: boolean;
 
 
28
  syncedAt?: string;
29
  error?: string;
30
  loading?: boolean;
@@ -554,12 +556,20 @@ export default function ClientsManagementView() {
554
 
555
  // ─── Meta status helpers ──────────────────────────────────────────────────────
556
 
 
 
 
 
 
 
 
 
 
557
  function dailyLimitLabel(ms?: MetaStatus): string {
558
  if (!ms || ms.loading) return '…';
559
  if (!ms.configured) return 'β€”';
560
- if (ms.wabaStatus === 'APPROVED') return '1 000+ conv.';
561
- if (ms.wabaStatus === 'REJECTED' || ms.wabaStatus === 'BANNED') return '0 conv.';
562
- return '250 conv.';
563
  }
564
 
565
  function WabaStatusBadge({ ms }: { ms?: MetaStatus }) {
@@ -649,7 +659,19 @@ function BusinessVerificationCell({ ms }: { ms?: MetaStatus }) {
649
  );
650
  }
651
 
 
 
 
 
 
 
 
 
 
 
 
652
  function DailyLimitCell({ ms }: { ms?: MetaStatus }) {
 
653
  return (
654
  <div className="flex items-center gap-3">
655
  <div className="p-2 bg-indigo-50 rounded-lg">
@@ -657,7 +679,13 @@ function DailyLimitCell({ ms }: { ms?: MetaStatus }) {
657
  </div>
658
  <div>
659
  <p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider">Limite Quotidienne</p>
660
- <p className="text-sm font-medium text-slate-700">{dailyLimitLabel(ms)}</p>
 
 
 
 
 
 
661
  </div>
662
  </div>
663
  );
 
25
  businessId?: string;
26
  businessName?: string;
27
  businessVerified?: boolean;
28
+ messagingLimitTier?: string;
29
+ qualityRating?: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN';
30
  syncedAt?: string;
31
  error?: string;
32
  loading?: boolean;
 
556
 
557
  // ─── Meta status helpers ──────────────────────────────────────────────────────
558
 
559
+ const TIER_LABELS: Record<string, string> = {
560
+ TIER_50: '50 conv./j',
561
+ TIER_250: '250 conv./j',
562
+ TIER_1K: '1 000 conv./j',
563
+ TIER_10K: '10 000 conv./j',
564
+ TIER_100K: '100 000 conv./j',
565
+ UNLIMITED: 'IllimitΓ©',
566
+ };
567
+
568
  function dailyLimitLabel(ms?: MetaStatus): string {
569
  if (!ms || ms.loading) return '…';
570
  if (!ms.configured) return 'β€”';
571
+ if (ms.messagingLimitTier) return TIER_LABELS[ms.messagingLimitTier] ?? ms.messagingLimitTier;
572
+ return 'β€”';
 
573
  }
574
 
575
  function WabaStatusBadge({ ms }: { ms?: MetaStatus }) {
 
659
  );
660
  }
661
 
662
+ function QualityDot({ rating }: { rating?: string }) {
663
+ const map: Record<string, string> = {
664
+ GREEN: 'bg-emerald-400',
665
+ YELLOW: 'bg-amber-400',
666
+ RED: 'bg-red-400',
667
+ UNKNOWN: 'bg-slate-300',
668
+ };
669
+ const cls = map[rating ?? 'UNKNOWN'] ?? 'bg-slate-300';
670
+ return <span className={`inline-block w-2 h-2 rounded-full ${cls}`} title={`QualitΓ© : ${rating ?? 'β€”'}`} />;
671
+ }
672
+
673
  function DailyLimitCell({ ms }: { ms?: MetaStatus }) {
674
+ const isLoading = !ms || ms.loading;
675
  return (
676
  <div className="flex items-center gap-3">
677
  <div className="p-2 bg-indigo-50 rounded-lg">
 
679
  </div>
680
  <div>
681
  <p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider">Limite Quotidienne</p>
682
+ <div className="flex items-center gap-1.5">
683
+ {isLoading
684
+ ? <span className="text-slate-400 text-xs animate-pulse">…</span>
685
+ : <p className="text-sm font-medium text-slate-700">{dailyLimitLabel(ms)}</p>
686
+ }
687
+ {!isLoading && ms?.qualityRating && <QualityDot rating={ms.qualityRating} />}
688
+ </div>
689
  </div>
690
  </div>
691
  );
apps/api/src/services/organization.ts CHANGED
@@ -88,6 +88,8 @@ export interface MetaStatus {
88
  businessId?: string;
89
  businessName?: string;
90
  businessVerified?: boolean;
 
 
91
  syncedAt: string;
92
  error?: string;
93
  }
@@ -194,12 +196,36 @@ export async function fetchMetaStatus(organizationId: string, forceRefresh = fal
194
  } catch { /* non-fatal */ }
195
  }
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  const result: MetaStatus = {
198
  configured: true,
199
  wabaStatus,
200
  businessId,
201
  businessName,
202
  businessVerified,
 
 
203
  syncedAt: new Date().toISOString(),
204
  ...(wabaError && { error: wabaError })
205
  };
 
88
  businessId?: string;
89
  businessName?: string;
90
  businessVerified?: boolean;
91
+ messagingLimitTier?: string; // e.g. TIER_1K, TIER_10K, TIER_100K, UNLIMITED
92
+ qualityRating?: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN';
93
  syncedAt: string;
94
  error?: string;
95
  }
 
196
  } catch { /* non-fatal */ }
197
  }
198
 
199
+ // Call 3 β€” Phone number messaging tier + quality rating (live from Meta)
200
+ let messagingLimitTier: string | undefined;
201
+ let qualityRating: MetaStatus['qualityRating'] | undefined;
202
+ try {
203
+ const orgWithPhones = await prisma.whatsAppPhoneNumber.findFirst({
204
+ where: { organizationId }
205
+ });
206
+ if (orgWithPhones?.id) {
207
+ const phoneRes = await fetch(
208
+ `https://graph.facebook.com/v19.0/${orgWithPhones.id}?fields=messaging_limit_tier,quality_rating`,
209
+ { headers }
210
+ );
211
+ const phoneData = await phoneRes.json() as any;
212
+ if (!phoneData.error) {
213
+ messagingLimitTier = phoneData.messaging_limit_tier;
214
+ qualityRating = phoneData.quality_rating;
215
+ } else {
216
+ logger.warn({ err: phoneData.error, organizationId }, '[META-STATUS] Phone tier fetch error');
217
+ }
218
+ }
219
+ } catch { /* non-fatal */ }
220
+
221
  const result: MetaStatus = {
222
  configured: true,
223
  wabaStatus,
224
  businessId,
225
  businessName,
226
  businessVerified,
227
+ messagingLimitTier,
228
+ qualityRating,
229
  syncedAt: new Date().toISOString(),
230
  ...(wabaError && { error: wabaError })
231
  };