Track payment status in org audit logs#1545
Conversation
📝 WalkthroughWalkthroughThis change adds upgrade tracking functionality across the system. It introduces new database columns to record organization upgrade timestamps and daily upgrade counts, updates type definitions to reflect the schema changes, implements upgrade event handling in Stripe integration with audit logging, and updates the admin dashboard to display upgrade metrics and trends alongside removal of the legacy need-upgrade chart. Changes
Sequence Diagram(s)sequenceDiagram
participant Stripe as Stripe (Event)
participant Handler as stripe_event.ts
participant DB as Database
participant Logsnag as logsnag_insights.ts
participant Dashboard as Admin Dashboard
Stripe->>Handler: Plan upgrade event received
Handler->>Handler: Compare with previousStripeInfo
Handler->>Handler: Set upgraded_at timestamp
Handler->>DB: Update stripe_info with upgraded_at
Handler->>DB: Insert audit_log entry
Note over Logsnag: Next aggregation cycle
Logsnag->>DB: Query stripe_info (last 24h upgrades)
DB-->>Logsnag: upgraded_orgs count
Logsnag->>DB: Insert into global_stats with upgraded_orgs
Logsnag->>Dashboard: Include in latestGlobalStats
Dashboard->>DB: Fetch globalStatsTrendData with upgraded_orgs
DB-->>Dashboard: Trend data by date
Dashboard->>Dashboard: Build upgradeTrendSeries (2 series)
Dashboard->>Dashboard: Render upgrade metrics & trend chart
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 SQLFluff (4.0.0)supabase/migrations/20260201015640_add_upgrade_org_stats.sqlUser Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a7964b2a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (stripeData.isUpgrade && stripeData.previousProductId) | ||
| updateData.upgraded_at = new Date().toISOString() |
There was a problem hiding this comment.
Only mark upgraded_at for true plan upgrades
The new upgrade metric keys off stripeData.isUpgrade, which is set whenever the subscription product changes, regardless of direction. That means downgrades or lateral plan changes will still set upgraded_at, and upgraded_orgs will count them as upgrades. This skews the “Upgraded Organizations (24h)” chart and any downstream metrics whenever a customer moves to a cheaper plan. Consider verifying the change is actually to a higher-tier plan (e.g., compare plan price/priority) before setting upgraded_at.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 17
🤖 Fix all issues with AI agents
In `@messages/es.json`:
- Around line 867-868: The Spanish locale file contains an English value for the
key "upgrade-trend"; update the "upgrade-trend" entry in messages/es.json to a
Spanish translation (e.g., "Tendencia de Actualización" or "Tendencia de
Actualizaciones") so it matches the localized "need-upgrade-trend" key and
maintains UI consistency.
In `@messages/fr.json`:
- Around line 867-868: The French bundle contains an English label for the key
"upgrade-trend"; update the value for "upgrade-trend" to a proper French
translation (e.g., "Tendance de mise à niveau") so it matches the localized key
"need-upgrade-trend" and stays consistent in messages/fr.json; locate the
"upgrade-trend" entry and replace the English string with the French string.
In `@messages/hi.json`:
- Line 868: Update the Hindi locale entry for the "upgrade-trend" key in
messages/hi.json so its value is translated into Hindi (replace the English
"Upgrade Trend" with an appropriate Hindi string such as "अपग्रेड प्रवृत्ति" or
"अपग्रेड रुझान"); ensure you only modify the value for the "upgrade-trend" key
and keep JSON formatting and surrounding keys intact.
In `@messages/id.json`:
- Around line 867-869: Translate the "upgrade-trend" locale key value into
Indonesian so the UI is fully localized; locate the "upgrade-trend" entry in
messages/id.json and replace the English string "Upgrade Trend" with an
appropriate Indonesian translation (e.g., "Tren Peningkatan" or another
preferred phrasing) while preserving the JSON key and formatting.
In `@messages/it.json`:
- Around line 867-868: The "upgrade-trend" i18n key value is still English;
update the Italian translation for the "upgrade-trend" key to match the
surrounding localized strings (e.g., replace "Upgrade Trend" with an Italian
phrase such as "Tendenza degli aggiornamenti" or "Tendenza di aggiornamento") so
it matches "need-upgrade-trend" and other Italian UI entries.
In `@messages/ja.json`:
- Line 868: The locale key "upgrade-trend" currently has an English value;
update messages/ja.json so the value is translated into Japanese by replacing
"Upgrade Trend" with a Japanese translation (e.g., "アップグレード傾向" or "アップグレードトレンド")
for the "upgrade-trend" key to keep the file consistent.
In `@messages/ko.json`:
- Line 868: Translate the new UI label key "upgrade-trend" in messages/ko.json
from English to Korean by replacing the current value "Upgrade Trend" with the
proper Korean translation (e.g., "업그레이드 추세") so the localized string for the
"upgrade-trend" key appears in Korean in the ko.json file.
In `@messages/pl.json`:
- Around line 867-868: The "upgrade-trend" entry in messages/pl.json is still in
English; update the value for the "upgrade-trend" key to a Polish translation
(e.g., "Trend aktualizacji") so the label matches the localized UI and stays
consistent with the adjacent "need-upgrade-trend" key.
In `@messages/pt-br.json`:
- Line 868: The JSON localization key "upgrade-trend" currently has an English
value ("Upgrade Trend"); update the pt-BR translation by replacing the value
with a Portuguese string such as "Tendência de upgrade" or "Tendência de
atualização" in the messages/pt-br.json entry for "upgrade-trend", preserving
JSON formatting and quotes.
In `@messages/ru.json`:
- Line 868: Replace the English value for the locale key "upgrade-trend" with
the Russian translation (e.g., "Тренд обновлений" or "Динамика обновлений") in
the ru locale JSON so the key "upgrade-trend" reads with the chosen Russian
string instead of "Upgrade Trend"; update only the value for the "upgrade-trend"
key in the messages/ru.json locale block.
In `@messages/tr.json`:
- Around line 867-868: The Turkish localization bundle contains an English value
for the key "upgrade-trend"; replace that English string with a proper Turkish
translation (e.g., set "upgrade-trend" to "Yükseltme Eğilimi" or another agreed
Turkish phrase) so both "need-upgrade-trend" and "upgrade-trend" are localized
consistently in messages/tr.json.
In `@messages/vi.json`:
- Around line 867-868: The Vietnamese locale contains an English string for the
key "upgrade-trend"; replace the English value ("Upgrade Trend") with the proper
Vietnamese translation (e.g., "Xu hướng Nâng Cấp" or another approved
translation) so both "need-upgrade-trend" and "upgrade-trend" are localized
consistently in the vi bundle.
In `@messages/zh-cn.json`:
- Line 868: The Simplified Chinese locale key "upgrade-trend" in
messages/zh-cn.json currently has an English value; update the value for the
"upgrade-trend" key to the Chinese translation (e.g., "升级趋势") so the UI displays
the localized label for Simplified Chinese.
In `@src/pages/admin/dashboard/revenue.vue`:
- Around line 127-149: The upgradeTrendSeries computed currently maps
globalStatsTrendData.value and uses item.need_upgrade directly, which can be
null; update the mapping in upgradeTrendSeries to provide a safe numeric
fallback (e.g., item.need_upgrade ?? 0 or Number(item.need_upgrade) || 0) for
the need_upgrade field so chart rendering never receives null—mirror how
upgraded_orgs is handled and ensure the returned data objects still use the same
shape (date, value).
- Around line 404-451: The template uses latestGlobalStats.need_upgrade directly
which can be null and causes toLocaleString() to throw; change the display
expression in the "Orgs Need Upgrade" card to coerce a numeric fallback (e.g.,
use optional chaining + nullish coalescing or a logical fallback) so you call
toLocaleString() on a number, similar to how (latestGlobalStats.upgraded_orgs ||
0) is handled; update the expression referencing latestGlobalStats.need_upgrade
in the template accordingly.
In `@supabase/functions/_backend/triggers/logsnag_insights.ts`:
- Around line 429-440: The new upgraded_orgs promise uses
supabase.from('stripe_info') which violates backend DB rules — replace this with
a query via getPgClient() or getDrizzleClient() from utils/pg.ts: use the same
filter (.gte equivalent) on upgraded_at >= last24h, SELECT customer_id (or
perform SELECT COUNT(DISTINCT customer_id)) against stripe_info, and return the
unique customer count; preserve the existing error handling pattern (cloudlog
with requestId and error) and ensure upgraded_orgs resolves to a number like
before. Locate the upgraded_orgs assignment in the stats block and swap the
supabase call for a client.query/execute using getPgClient() or
getDrizzleClient(), mapping returned rows to a Set or using COUNT(DISTINCT ...)
to compute the size.
In `@supabase/functions/_backend/triggers/stripe_event.ts`:
- Around line 271-306: Replace the supabaseAdmin insert in logStripeInfoAudit
with the project-standard Postgres client: obtain a client via
getDrizzleClient() (or getPgClient()) from utils/pg.ts, build and execute an
INSERT into audit_logs using the drizzle API (table audit_logs, fields:
table_name, record_id, operation, user_id, org_id, old_record, new_record,
changed_fields) and handle the returned error the same way (calling cloudlog
with requestId). Keep getStripeInfoChangedFields and the existing
newRecord/changedFields logic unchanged; only swap the supabaseAdmin(...)
.from(...).insert(...) call to a drizzle insert using the appropriate client
acquisition and error check.
🧹 Nitpick comments (1)
messages/de.json (1)
867-869: Consider German phrasing consistency forupgrade-trend.
Maybe use “Upgrade‑Trend” or “Upgrade‑Verlauf” to match German hyphenation/style.📝 Possible wording tweak
- "upgrade-trend": "Upgrade Trend", + "upgrade-trend": "Upgrade‑Trend",
| "need-upgrade-trend": "Organizaciones que Necesitan Actualización", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
Localize the new label for Spanish UI consistency.
"Upgrade Trend" is English; please translate for the ES locale.
💡 Suggested translation
- "upgrade-trend": "Upgrade Trend",
+ "upgrade-trend": "Tendencia de actualización",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "need-upgrade-trend": "Organizaciones que Necesitan Actualización", | |
| "upgrade-trend": "Upgrade Trend", | |
| "need-upgrade-trend": "Organizaciones que Necesitan Actualización", | |
| "upgrade-trend": "Tendencia de actualización", |
🤖 Prompt for AI Agents
In `@messages/es.json` around lines 867 - 868, The Spanish locale file contains an
English value for the key "upgrade-trend"; update the "upgrade-trend" entry in
messages/es.json to a Spanish translation (e.g., "Tendencia de Actualización" or
"Tendencia de Actualizaciones") so it matches the localized "need-upgrade-trend"
key and maintains UI consistency.
| "need-upgrade-trend": "Organisations Nécessitant une Mise à Niveau", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
Localize the new label for French.
Line 868 uses English text in the French bundle; it should be translated for consistency.
💡 Suggested localization
- "upgrade-trend": "Upgrade Trend",
+ "upgrade-trend": "Tendance de mise à niveau",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "need-upgrade-trend": "Organisations Nécessitant une Mise à Niveau", | |
| "upgrade-trend": "Upgrade Trend", | |
| "need-upgrade-trend": "Organisations Nécessitant une Mise à Niveau", | |
| "upgrade-trend": "Tendance de mise à niveau", |
🤖 Prompt for AI Agents
In `@messages/fr.json` around lines 867 - 868, The French bundle contains an
English label for the key "upgrade-trend"; update the value for "upgrade-trend"
to a proper French translation (e.g., "Tendance de mise à niveau") so it matches
the localized key "need-upgrade-trend" and stays consistent in messages/fr.json;
locate the "upgrade-trend" entry and replace the English string with the French
string.
| "native-dependencies-description": "इस बंडल में शामिल नेटिव पैकेज और उनके संस्करण", | ||
| "need-more-contact-us": "अधिक चाहिए? हमसे संपर्क करें तैयार की गई योजना के लिए", | ||
| "need-upgrade-trend": "संगठनों को अपग्रेड की आवश्यकता", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
Localize the new label for Hindi UI.
The value is still in English; please translate it (e.g., “अपग्रेड प्रवृत्ति/रुझान”).
🤖 Prompt for AI Agents
In `@messages/hi.json` at line 868, Update the Hindi locale entry for the
"upgrade-trend" key in messages/hi.json so its value is translated into Hindi
(replace the English "Upgrade Trend" with an appropriate Hindi string such as
"अपग्रेड प्रवृत्ति" or "अपग्रेड रुझान"); ensure you only modify the value for
the "upgrade-trend" key and keep JSON formatting and surrounding keys intact.
| "need-upgrade-trend": "Organisasi Membutuhkan Peningkatan", | ||
| "upgrade-trend": "Upgrade Trend", | ||
| "never": "Tidak pernah", |
There was a problem hiding this comment.
Localize the new upgrade trend label.
The Indonesian locale entry is still in English, which can create mixed-language UI. Consider translating it to Indonesian.
💡 Suggested fix
- "upgrade-trend": "Upgrade Trend",
+ "upgrade-trend": "Tren Peningkatan",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "need-upgrade-trend": "Organisasi Membutuhkan Peningkatan", | |
| "upgrade-trend": "Upgrade Trend", | |
| "never": "Tidak pernah", | |
| "need-upgrade-trend": "Organisasi Membutuhkan Peningkatan", | |
| "upgrade-trend": "Tren Peningkatan", | |
| "never": "Tidak pernah", |
🤖 Prompt for AI Agents
In `@messages/id.json` around lines 867 - 869, Translate the "upgrade-trend"
locale key value into Indonesian so the UI is fully localized; locate the
"upgrade-trend" entry in messages/id.json and replace the English string
"Upgrade Trend" with an appropriate Indonesian translation (e.g., "Tren
Peningkatan" or another preferred phrasing) while preserving the JSON key and
formatting.
| "need-upgrade-trend": "Organizzazioni che necessitano di un aggiornamento", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
Localize the new label for Italian UI consistency.
"Upgrade Trend" is still English while surrounding strings are Italian. Consider translating it.
💡 Suggested translation
- "upgrade-trend": "Upgrade Trend",
+ "upgrade-trend": "Tendenza degli upgrade",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "need-upgrade-trend": "Organizzazioni che necessitano di un aggiornamento", | |
| "upgrade-trend": "Upgrade Trend", | |
| "need-upgrade-trend": "Organizzazioni che necessitano di un aggiornamento", | |
| "upgrade-trend": "Tendenza degli upgrade", |
🤖 Prompt for AI Agents
In `@messages/it.json` around lines 867 - 868, The "upgrade-trend" i18n key value
is still English; update the Italian translation for the "upgrade-trend" key to
match the surrounding localized strings (e.g., replace "Upgrade Trend" with an
Italian phrase such as "Tendenza degli aggiornamenti" or "Tendenza di
aggiornamento") so it matches "need-upgrade-trend" and other Italian UI entries.
| "native-dependencies-description": "此包中包含的原生包及其版本", | ||
| "need-more-contact-us": "需要更多 ?\n联系我们获取量身定制的计划", | ||
| "need-upgrade-trend": "需要升级的组织", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
Localize the new label for Simplified Chinese UI.
The value is still in English; please translate it (e.g., “升级趋势”).
🤖 Prompt for AI Agents
In `@messages/zh-cn.json` at line 868, The Simplified Chinese locale key
"upgrade-trend" in messages/zh-cn.json currently has an English value; update
the value for the "upgrade-trend" key to the Chinese translation (e.g., "升级趋势")
so the UI displays the localized label for Simplified Chinese.
| const upgradeTrendSeries = computed(() => { | ||
| if (globalStatsTrendData.value.length === 0) | ||
| return [] | ||
|
|
||
| return [ | ||
| { | ||
| label: 'Organizations Needing Upgrade', | ||
| data: globalStatsTrendData.value.map(item => ({ | ||
| date: item.date, | ||
| value: item.need_upgrade, | ||
| })), | ||
| color: '#ef4444', // red | ||
| }, | ||
| { | ||
| label: 'Upgraded Organizations (24h)', | ||
| data: globalStatsTrendData.value.map(item => ({ | ||
| date: item.date, | ||
| value: item.upgraded_orgs || 0, | ||
| })), | ||
| color: '#10b981', // green | ||
| }, | ||
| ] | ||
| }) |
There was a problem hiding this comment.
Guard against nullable need_upgrade in the trend series.
need_upgrade can be null in the DB schema, which may break chart rendering. Use a fallback.
🛠️ Suggested fix
- value: item.need_upgrade,
+ value: item.need_upgrade ?? 0,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const upgradeTrendSeries = computed(() => { | |
| if (globalStatsTrendData.value.length === 0) | |
| return [] | |
| return [ | |
| { | |
| label: 'Organizations Needing Upgrade', | |
| data: globalStatsTrendData.value.map(item => ({ | |
| date: item.date, | |
| value: item.need_upgrade, | |
| })), | |
| color: '#ef4444', // red | |
| }, | |
| { | |
| label: 'Upgraded Organizations (24h)', | |
| data: globalStatsTrendData.value.map(item => ({ | |
| date: item.date, | |
| value: item.upgraded_orgs || 0, | |
| })), | |
| color: '#10b981', // green | |
| }, | |
| ] | |
| }) | |
| const upgradeTrendSeries = computed(() => { | |
| if (globalStatsTrendData.value.length === 0) | |
| return [] | |
| return [ | |
| { | |
| label: 'Organizations Needing Upgrade', | |
| data: globalStatsTrendData.value.map(item => ({ | |
| date: item.date, | |
| value: item.need_upgrade ?? 0, | |
| })), | |
| color: '#ef4444', // red | |
| }, | |
| { | |
| label: 'Upgraded Organizations (24h)', | |
| data: globalStatsTrendData.value.map(item => ({ | |
| date: item.date, | |
| value: item.upgraded_orgs || 0, | |
| })), | |
| color: '#10b981', // green | |
| }, | |
| ] | |
| }) |
🤖 Prompt for AI Agents
In `@src/pages/admin/dashboard/revenue.vue` around lines 127 - 149, The
upgradeTrendSeries computed currently maps globalStatsTrendData.value and uses
item.need_upgrade directly, which can be null; update the mapping in
upgradeTrendSeries to provide a safe numeric fallback (e.g., item.need_upgrade
?? 0 or Number(item.need_upgrade) || 0) for the need_upgrade field so chart
rendering never receives null—mirror how upgraded_orgs is handled and ensure the
returned data objects still use the same shape (date, value).
| <!-- Upgrade Metrics Cards --> | ||
| <div class="grid grid-cols-1 gap-6 md:grid-cols-2"> | ||
| <!-- Organizations Needing Upgrade --> | ||
| <div class="flex flex-col justify-between p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900"> | ||
| <div class="flex items-start justify-between mb-4"> | ||
| <div class="p-3 rounded-lg bg-error/10"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current text-error"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v4m0 4h.01M5.07 19h13.86a2 2 0 001.74-3l-6.93-12a2 2 0 00-3.48 0l-6.93 12a2 2 0 001.74 3z" /></svg> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <p class="text-sm text-slate-600 dark:text-slate-400"> | ||
| Orgs Need Upgrade | ||
| </p> | ||
| <p v-if="latestGlobalStats" class="mt-2 text-3xl font-bold text-error"> | ||
| {{ latestGlobalStats.need_upgrade.toLocaleString() }} | ||
| </p> | ||
| <p v-else class="mt-2 text-3xl font-bold text-error"> | ||
| 0 | ||
| </p> | ||
| <p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> | ||
| Organizations over plan limits | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Organizations Upgraded --> | ||
| <div class="flex flex-col justify-between p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900"> | ||
| <div class="flex items-start justify-between mb-4"> | ||
| <div class="p-3 rounded-lg bg-success/10"> | ||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current text-success"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <p class="text-sm text-slate-600 dark:text-slate-400"> | ||
| Orgs Upgraded (24h) | ||
| </p> | ||
| <p v-if="latestGlobalStats" class="mt-2 text-3xl font-bold text-success"> | ||
| {{ (latestGlobalStats.upgraded_orgs || 0).toLocaleString() }} | ||
| </p> | ||
| <p v-else class="mt-2 text-3xl font-bold text-success"> | ||
| 0 | ||
| </p> | ||
| <p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> | ||
| Plan upgrades in the last 24 hours | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Avoid toLocaleString() on possibly null need_upgrade.
If need_upgrade is null, this will throw at runtime. Use a numeric fallback.
🛠️ Suggested fix
- {{ latestGlobalStats.need_upgrade.toLocaleString() }}
+ {{ (latestGlobalStats.need_upgrade ?? 0).toLocaleString() }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <!-- Upgrade Metrics Cards --> | |
| <div class="grid grid-cols-1 gap-6 md:grid-cols-2"> | |
| <!-- Organizations Needing Upgrade --> | |
| <div class="flex flex-col justify-between p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900"> | |
| <div class="flex items-start justify-between mb-4"> | |
| <div class="p-3 rounded-lg bg-error/10"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current text-error"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v4m0 4h.01M5.07 19h13.86a2 2 0 001.74-3l-6.93-12a2 2 0 00-3.48 0l-6.93 12a2 2 0 001.74 3z" /></svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-sm text-slate-600 dark:text-slate-400"> | |
| Orgs Need Upgrade | |
| </p> | |
| <p v-if="latestGlobalStats" class="mt-2 text-3xl font-bold text-error"> | |
| {{ latestGlobalStats.need_upgrade.toLocaleString() }} | |
| </p> | |
| <p v-else class="mt-2 text-3xl font-bold text-error"> | |
| 0 | |
| </p> | |
| <p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> | |
| Organizations over plan limits | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Organizations Upgraded --> | |
| <div class="flex flex-col justify-between p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900"> | |
| <div class="flex items-start justify-between mb-4"> | |
| <div class="p-3 rounded-lg bg-success/10"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current text-success"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-sm text-slate-600 dark:text-slate-400"> | |
| Orgs Upgraded (24h) | |
| </p> | |
| <p v-if="latestGlobalStats" class="mt-2 text-3xl font-bold text-success"> | |
| {{ (latestGlobalStats.upgraded_orgs || 0).toLocaleString() }} | |
| </p> | |
| <p v-else class="mt-2 text-3xl font-bold text-success"> | |
| 0 | |
| </p> | |
| <p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> | |
| Plan upgrades in the last 24 hours | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Upgrade Metrics Cards --> | |
| <div class="grid grid-cols-1 gap-6 md:grid-cols-2"> | |
| <!-- Organizations Needing Upgrade --> | |
| <div class="flex flex-col justify-between p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900"> | |
| <div class="flex items-start justify-between mb-4"> | |
| <div class="p-3 rounded-lg bg-error/10"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current text-error"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v4m0 4h.01M5.07 19h13.86a2 2 0 001.74-3l-6.93-12a2 2 0 00-3.48 0l-6.93 12a2 2 0 001.74 3z" /></svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-sm text-slate-600 dark:text-slate-400"> | |
| Orgs Need Upgrade | |
| </p> | |
| <p v-if="latestGlobalStats" class="mt-2 text-3xl font-bold text-error"> | |
| {{ (latestGlobalStats.need_upgrade ?? 0).toLocaleString() }} | |
| </p> | |
| <p v-else class="mt-2 text-3xl font-bold text-error"> | |
| 0 | |
| </p> | |
| <p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> | |
| Organizations over plan limits | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Organizations Upgraded --> | |
| <div class="flex flex-col justify-between p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900"> | |
| <div class="flex items-start justify-between mb-4"> | |
| <div class="p-3 rounded-lg bg-success/10"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current text-success"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-sm text-slate-600 dark:text-slate-400"> | |
| Orgs Upgraded (24h) | |
| </p> | |
| <p v-if="latestGlobalStats" class="mt-2 text-3xl font-bold text-success"> | |
| {{ (latestGlobalStats.upgraded_orgs || 0).toLocaleString() }} | |
| </p> | |
| <p v-else class="mt-2 text-3xl font-bold text-success"> | |
| 0 | |
| </p> | |
| <p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> | |
| Plan upgrades in the last 24 hours | |
| </p> | |
| </div> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
In `@src/pages/admin/dashboard/revenue.vue` around lines 404 - 451, The template
uses latestGlobalStats.need_upgrade directly which can be null and causes
toLocaleString() to throw; change the display expression in the "Orgs Need
Upgrade" card to coerce a numeric fallback (e.g., use optional chaining +
nullish coalescing or a logical fallback) so you call toLocaleString() on a
number, similar to how (latestGlobalStats.upgraded_orgs || 0) is handled; update
the expression referencing latestGlobalStats.need_upgrade in the template
accordingly.
| upgraded_orgs: supabase | ||
| .from('stripe_info') | ||
| .select('customer_id', { count: 'exact', head: false }) | ||
| .gte('upgraded_at', last24h) | ||
| .then((res) => { | ||
| if (res.error) { | ||
| cloudlog({ requestId: c.get('requestId'), message: 'upgraded_orgs error', error: res.error }) | ||
| return 0 | ||
| } | ||
| const uniqueCustomers = new Set((res.data || []).map(row => row.customer_id)) | ||
| return uniqueCustomers.size | ||
| }), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Align the new upgraded_orgs query with backend DB access rules.
This adds another supabaseAdmin-based query in _backend, while backend guidelines require using getPgClient()/getDrizzleClient() for DB access. Please migrate this query (or the whole stats block) to the prescribed clients to avoid drift during the D1 migration.
As per coding guidelines: All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1.
🤖 Prompt for AI Agents
In `@supabase/functions/_backend/triggers/logsnag_insights.ts` around lines 429 -
440, The new upgraded_orgs promise uses supabase.from('stripe_info') which
violates backend DB rules — replace this with a query via getPgClient() or
getDrizzleClient() from utils/pg.ts: use the same filter (.gte equivalent) on
upgraded_at >= last24h, SELECT customer_id (or perform SELECT COUNT(DISTINCT
customer_id)) against stripe_info, and return the unique customer count;
preserve the existing error handling pattern (cloudlog with requestId and error)
and ensure upgraded_orgs resolves to a number like before. Locate the
upgraded_orgs assignment in the stats block and swap the supabase call for a
client.query/execute using getPgClient() or getDrizzleClient(), mapping returned
rows to a Set or using COUNT(DISTINCT ...) to compute the size.
| function getStripeInfoChangedFields(oldRecord: Record<string, any> | null, newRecord: Record<string, any>, updateFields: string[]): string[] { | ||
| if (!oldRecord) | ||
| return updateFields | ||
|
|
||
| return updateFields.filter((field) => { | ||
| if (!(field in newRecord)) | ||
| return false | ||
| return oldRecord[field] !== newRecord[field] | ||
| }) | ||
| } | ||
|
|
||
| async function logStripeInfoAudit(c: Context, orgId: string, customerId: string, oldRecord: Record<string, any> | null, updateData: Record<string, any>) { | ||
| const newRecord = oldRecord ? { ...oldRecord, ...updateData } : { ...updateData } | ||
| const updateFields = Object.keys(updateData) | ||
| const changedFields = getStripeInfoChangedFields(oldRecord, newRecord, updateFields) | ||
|
|
||
| if (changedFields.length === 0) | ||
| return | ||
|
|
||
| const { error } = await supabaseAdmin(c) | ||
| .from('audit_logs') | ||
| .insert({ | ||
| table_name: 'stripe_info', | ||
| record_id: customerId, | ||
| operation: 'UPDATE', | ||
| user_id: null, | ||
| org_id: orgId, | ||
| old_record: oldRecord, | ||
| new_record: newRecord, | ||
| changed_fields: changedFields, | ||
| }) | ||
|
|
||
| if (error) { | ||
| cloudlog({ requestId: c.get('requestId'), message: 'audit_logs stripe_info insert error', error }) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use getPgClient/getDrizzleClient for the new audit insert.
logStripeInfoAudit writes to the DB via supabaseAdmin, but backend guidelines require using the pg/drizzle clients in _backend. Please switch this insert to drizzle to stay aligned with the migration path.
🔧 Suggested refactor (drizzle insert)
async function logStripeInfoAudit(c: Context, orgId: string, customerId: string, oldRecord: Record<string, any> | null, updateData: Record<string, any>) {
const newRecord = oldRecord ? { ...oldRecord, ...updateData } : { ...updateData }
const updateFields = Object.keys(updateData)
const changedFields = getStripeInfoChangedFields(oldRecord, newRecord, updateFields)
if (changedFields.length === 0)
return
- const { error } = await supabaseAdmin(c)
- .from('audit_logs')
- .insert({
- table_name: 'stripe_info',
- record_id: customerId,
- operation: 'UPDATE',
- user_id: null,
- org_id: orgId,
- old_record: oldRecord,
- new_record: newRecord,
- changed_fields: changedFields,
- })
-
- if (error) {
- cloudlog({ requestId: c.get('requestId'), message: 'audit_logs stripe_info insert error', error })
- }
+ const pgClient = getPgClient(c, true)
+ const drizzleClient = getDrizzleClient(pgClient)
+ try {
+ await drizzleClient.insert(schema.audit_logs).values({
+ table_name: 'stripe_info',
+ record_id: customerId,
+ operation: 'UPDATE',
+ user_id: null,
+ org_id: orgId,
+ old_record: oldRecord,
+ new_record: newRecord,
+ changed_fields: changedFields,
+ })
+ }
+ catch (error) {
+ cloudlog({ requestId: c.get('requestId'), message: 'audit_logs stripe_info insert error', error })
+ }
+ finally {
+ closeClient(c, pgClient)
+ }
}As per coding guidelines: All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1.
🤖 Prompt for AI Agents
In `@supabase/functions/_backend/triggers/stripe_event.ts` around lines 271 - 306,
Replace the supabaseAdmin insert in logStripeInfoAudit with the project-standard
Postgres client: obtain a client via getDrizzleClient() (or getPgClient()) from
utils/pg.ts, build and execute an INSERT into audit_logs using the drizzle API
(table audit_logs, fields: table_name, record_id, operation, user_id, org_id,
old_record, new_record, changed_fields) and handle the returned error the same
way (calling cloudlog with requestId). Keep getStripeInfoChangedFields and the
existing newRecord/changedFields logic unchanged; only swap the
supabaseAdmin(...) .from(...).insert(...) call to a drizzle insert using the
appropriate client acquisition and error check.
There was a problem hiding this comment.
Pull request overview
This PR extends billing observability and analytics by logging Stripe subscription changes into the org audit logs and by adding upgrade-related metrics to the global stats and admin revenue dashboard.
Changes:
- Adds
upgraded_attostripe_infoandupgraded_orgstoglobal_stats, wires them through Supabase type definitions, and surfaces them via thegetAdminGlobalStatsTrendquery and thelogsnag_insightscron. - Introduces Stripe audit logging in
stripe_eventso changes tostripe_info(including upgrades) are recorded inaudit_logswith old/new record snapshots andchanged_fields. - Moves/expands upgrade-related admin metrics from the main admin dashboard into the revenue dashboard, including new cards and an “Upgrade Trend” chart, and adds the corresponding i18n keys.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
supabase/migrations/20260201015640_add_upgrade_org_stats.sql |
Adds stripe_info.upgraded_at and global_stats.upgraded_orgs columns to support upgrade tracking in analytics. |
supabase/functions/_backend/utils/supabase.types.ts |
Updates backend Supabase-generated types to include upgraded_orgs on global_stats and upgraded_at on stripe_info. |
supabase/functions/_backend/utils/pg.ts |
Extends AdminGlobalStatsTrend and the getAdminGlobalStatsTrend query/mapping to include upgraded_orgs. |
supabase/functions/_backend/triggers/stripe_event.ts |
Adds getStripeInfoChangedFields/logStripeInfoAudit, records Stripe-driven updates to stripe_info into audit_logs, and sets upgraded_at on upgrade events. |
supabase/functions/_backend/triggers/logsnag_insights.ts |
Computes upgraded_orgs from stripe_info.upgraded_at over the last 24h and persists it into global_stats alongside existing metrics. |
src/types/supabase.types.ts |
Mirrors the Supabase type changes on the frontend so UI code can consume upgraded_orgs and upgraded_at. |
src/pages/admin/dashboard/revenue.vue |
Adds upgrade-focused cards and an “Upgrade Trend” chart using need_upgrade and the new upgraded_orgs field from global_stats_trend. |
src/pages/admin/dashboard/index.vue |
Removes the “Need Upgrade Trend” chart from the main admin dashboard, consolidating upgrade metrics into the revenue page. |
messages/en.json |
Defines the new upgrade-trend i18n key used for the revenue dashboard chart title. |
messages/*.json (non-en locales) |
Adds upgrade-trend entries but currently leaves them as the English string “Upgrade Trend”, creating mixed-language UI labels in localized dashboards. |
| async function logStripeInfoAudit(c: Context, orgId: string, customerId: string, oldRecord: Record<string, any> | null, updateData: Record<string, any>) { | ||
| const newRecord = oldRecord ? { ...oldRecord, ...updateData } : { ...updateData } | ||
| const updateFields = Object.keys(updateData) | ||
| const changedFields = getStripeInfoChangedFields(oldRecord, newRecord, updateFields) | ||
|
|
||
| if (changedFields.length === 0) | ||
| return | ||
|
|
||
| const { error } = await supabaseAdmin(c) | ||
| .from('audit_logs') | ||
| .insert({ | ||
| table_name: 'stripe_info', | ||
| record_id: customerId, | ||
| operation: 'UPDATE', | ||
| user_id: null, | ||
| org_id: orgId, | ||
| old_record: oldRecord, | ||
| new_record: newRecord, | ||
| changed_fields: changedFields, | ||
| }) | ||
|
|
||
| if (error) { | ||
| cloudlog({ requestId: c.get('requestId'), message: 'audit_logs stripe_info insert error', error }) | ||
| } |
There was a problem hiding this comment.
Because new_record is built from the previously-fetched oldRecord plus updateData, it does not reflect changes made by database-side triggers (notably the handle_updated_at trigger on stripe_info.updated_at), so audit_logs.new_record.updated_at will be stale compared to the actual row after this update. If you want audit_logs.new_record to match the true post-update state (as described in the DB comments), consider either re-reading the updated stripe_info row after the update before logging, or explicitly excluding updated_at (and any other DB-managed fields) from changed_fields/new_record to avoid confusion.
| "native-dependencies-description": "このバンドルに含まれるネイティブパッケージとそのバージョン", | ||
| "need-more-contact-us": "もっと必要ですか?カスタムプランについてはお問い合わせください。", | ||
| "need-upgrade-trend": "アップグレードが必要な組織", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
In this Japanese locale file, the "upgrade-trend" entry still shows the English text "Upgrade Trend" while the surrounding keys (for example "need-upgrade-trend") are translated to Japanese. Consider translating this label to Japanese to maintain a fully localized admin dashboard.
| "upgrade-trend": "Upgrade Trend", | |
| "upgrade-trend": "アップグレード傾向", |
| "native-dependencies-description": "Paquetes nativos y sus versiones incluidos en este bundle", | ||
| "need-more-contact-us": "¿Necesitas más? Contáctanos para un plan personalizado", | ||
| "need-upgrade-trend": "Organizaciones que Necesitan Actualización", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
In this Spanish locale file, the "upgrade-trend" translation is currently the English phrase "Upgrade Trend", whereas "need-upgrade-trend" and other nearby strings are localized into Spanish. To avoid mixed-language labels, please add a Spanish translation for this new key.
| "upgrade-trend": "Upgrade Trend", | |
| "upgrade-trend": "Tendencia de Actualización", |
| "native-dependencies-description": "Native Pakete und deren Versionen in diesem Bundle", | ||
| "need-more-contact-us": "Brauchen Sie mehr? Kontaktieren Sie uns für einen maßgeschneiderten Plan", | ||
| "need-upgrade-trend": "Organisationen benötigen ein Upgrade", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
In this German locale file, the new "upgrade-trend" entry is left as the English text "Upgrade Trend" while adjacent labels like "need-upgrade-trend" are translated into German. Consider adding a proper German translation for this key so the admin dashboard remains fully localized.
| "upgrade-trend": "Upgrade Trend", | |
| "upgrade-trend": "Upgrade-Trend", |
| "native-dependencies-description": "此包中包含的原生包及其版本", | ||
| "need-more-contact-us": "需要更多 ?\n联系我们获取量身定制的计划", | ||
| "need-upgrade-trend": "需要升级的组织", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
In this Simplified Chinese locale file, the new "upgrade-trend" string is left as the English text "Upgrade Trend" while nearby keys such as "need-upgrade-trend" are localized, which leads to inconsistent UI language. Please provide a Chinese translation for this key (or mark it for translation) to keep the locale consistent.
| "upgrade-trend": "Upgrade Trend", | |
| "upgrade-trend": "升级趋势", |
| "native-dependencies-description": "Pacotes nativos e suas versões incluídos neste bundle", | ||
| "need-more-contact-us": "Precisa de mais? Entre em contato conosco para um plano personalizado", | ||
| "need-upgrade-trend": "Organizações Precisando de Atualização", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
In this Brazilian Portuguese locale file, the new "upgrade-trend" string is currently the English text "Upgrade Trend", while "need-upgrade-trend" and other entries are localized. Please provide a Brazilian Portuguese translation here to keep the dashboard labels consistently localized.
| "upgrade-trend": "Upgrade Trend", | |
| "upgrade-trend": "Tendência de Atualização de Plano", |
| "native-dependencies-description": "Paquets natifs et leurs versions inclus dans ce bundle", | ||
| "need-more-contact-us": "Besoin de plus ? Contactez-nous pour un plan sur mesure", | ||
| "need-upgrade-trend": "Organisations Nécessitant une Mise à Niveau", | ||
| "upgrade-trend": "Upgrade Trend", |
There was a problem hiding this comment.
In this French locale file, the new "upgrade-trend" value is the English text "Upgrade Trend" even though neighboring keys such as "need-upgrade-trend" are in French. For consistency in the French UI, consider supplying a French translation instead of leaving this in English.
| "upgrade-trend": "Upgrade Trend", | |
| "upgrade-trend": "Tendance des Mises à Niveau", |
| const originalStatus = stripeData.data.status | ||
| stripeData.data.status = 'succeeded' | ||
| await createdOrUpdated(c, stripeData, org, LogSnag, originalStatus!) | ||
| await createdOrUpdated(c, stripeData, org, LogSnag, originalStatus!, customer ?? null) |
There was a problem hiding this comment.
This use of variable 'customer' always evaluates to true.
| await trackBentoEvent(c, org.management_email, {}, 'org:failed_payment') | ||
| // Update the database with failed status | ||
| await updateStripeInfo(c, stripeData) | ||
| await updateStripeInfo(c, stripeData, org, customer ?? null) |
There was a problem hiding this comment.
This use of variable 'customer' always evaluates to true.
| } | ||
| // Otherwise keep it as 'canceled' since the period has ended | ||
| await updateStripeInfo(c, stripeData) | ||
| await updateStripeInfo(c, stripeData, org, customer ?? null) |
There was a problem hiding this comment.
This use of variable 'customer' always evaluates to true.
|



Summary (AI generated)
Test plan (AI generated)
Screenshots (AI generated)
Checklist (AI generated)
.
accordingly.
my tests
Generated with AI
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.