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 filesfetchMetaStatus 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.
|
| 561 |
-
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
};
|