[codex] Add credit analytics and Stripe customer country sync#1875
[codex] Add credit analytics and Stripe customer country sync#1875
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds admin credits and email-type analytics (UI, loaders, charts, and i18n), email classification utilities and backend PG queries, chart formatting props, Stripe customer-country sync (helpers, webhook handling, types, migration, and tests), plus related type and minor backend tweaks. Changes
Sequence Diagram(s)sequenceDiagram
participant Dashboard as Admin Dashboard (credits.vue / users.vue)
participant Store as Admin Store
participant Backend as Admin Stats Function
participant PG as PG Utils (getAdminGlobalStatsTrend / getAdminEmailTypeBreakdown)
participant DB as PostgreSQL
Dashboard->>Store: fetchStats(metric_category, start_date, end_date)
Store->>Backend: POST /admin_stats (metric_category, dates)
Backend->>PG: invoke query for metric_category
PG->>DB: SQL (generate_series, joins, aggregates)
DB-->>PG: rows (trend, totals)
PG-->>Backend: formatted payload
Backend-->>Store: metric payload
Store-->>Dashboard: update reactive state -> render charts
sequenceDiagram
participant Stripe as Stripe (webhook)
participant Webhook as Webhook Handler (stripe_event.ts)
participant StripeUtils as Stripe Utils (syncStripeCustomerCountry)
participant StripeAPI as Stripe API Client
participant DB as PostgreSQL
Stripe->>Webhook: POST event (customer.created / customer.updated)
Webhook->>Webhook: isCustomerProfileEvent? (true)
Webhook->>StripeUtils: syncStripeCustomerCountry(customerId)
StripeUtils->>StripeAPI: retrieve customer
StripeAPI-->>StripeUtils: Stripe.Customer (address.country)
StripeUtils->>StripeUtils: normalize country code
StripeUtils->>DB: UPDATE stripe_info SET customer_country = ...
DB-->>StripeUtils: update result
StripeUtils-->>Webhook: return normalized country
Webhook-->>Stripe: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a8fbbf695
ℹ️ 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".
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/admin/dashboard/credits.vue`:
- Around line 114-129: loadCreditAnalytics can suffer from out-of-order
responses (concurrent calls from onMounted, date-range changes, refreshes)
causing stale data to overwrite current state; fix by adding a request guard:
either attach an AbortController to each call and abort the previous controller
before calling adminStore.fetchStats, or use an incrementing requestId/token
stored on the component (e.g., lastCreditAnalyticsRequestId) and capture the
current token before awaiting adminStore.fetchStats, then verify the token
matches before assigning globalStatsTrendData and isLoadingCreditAnalytics;
apply the same pattern to the other similar functions at the referenced ranges
(lines ~394-400 and ~418-421) so only the latest response updates the reactive
state.
- Line 124: Replace all hard-coded English strings in credits.vue (including the
toast call toast.error('Failed to load credit analytics') and the chart labels,
titles and descriptive text referenced around lines 163-176, 187-200, 211,
443-447, and 454-478) with calls to the translation helper t('...') using
descriptive keys like admin.dashboard.credits.error.loadAnalytics,
admin.dashboard.credits.chart.title, admin.dashboard.credits.chart.labelX,
admin.dashboard.credits.description, etc.; then add matching keys and English
values into messages/en.json so there is no inline English text left in the
component and the UI localizes correctly. Ensure key names are consistent and
descriptive to locate them from the component.
In `@src/pages/admin/dashboard/users.vue`:
- Around line 753-832: Replace the hardcoded English strings in the Email Type
Breakdown block of users.vue (e.g., the header "Email Type Breakdown", card
titles "Professional Emails"/"Personal Emails"/"Disposable Emails", descriptions
like "Work and company domains", and the ChartCard title "Email Type Trend")
with i18n lookups using t('<key>') in the template (so AdminMultiLineChart and
ChartCard props and displayed text use t(...) instead of literals); then add the
corresponding keys (e.g., "email-type-breakdown", "professional-emails",
"personal-emails", "disposable-emails", "work-and-company-domains",
"public-mailbox-providers", "temporary-mailbox-providers", "email-type-trend")
to messages/en.json. Ensure you do not pass inline fallback text and update any
bound props like :title to use the t(...) call.
In `@supabase/functions/_backend/utils/pg.ts`:
- Around line 1228-1245: The date range is currently inclusive twice: the
date_series uses endDateOnly and normalized_users uses <= end_date, causing
boundary-day duplication; change the query so date_series generates up to
(${endDateOnly}::date - interval '1 day') instead of ${endDateOnly}::date, and
change the normalized_users filter from AND u.created_at <=
${end_date}::timestamptz to AND u.created_at < ${end_date}::timestamptz (leave
start/date conversions using startDateOnly and endDateOnly as-is). This ensures
the WITH date_series and the normalized_users WHERE use the same exclusive
end_date boundary.
In `@supabase/functions/_backend/utils/stripe.ts`:
- Around line 278-314: getStripeCustomerCountry currently returns null for both
"no country" and "API failure", causing syncStripeCustomerCountry to overwrite
stored customer_country on transient Stripe errors; change
getStripeCustomerCountry to NOT swallow Stripe API errors (remove or rethrow in
the catch) so it returns null only when the customer truly has no country, and
then update syncStripeCustomerCountry to call getStripeCustomerCountry inside a
try/catch and only perform the supabaseAdmin .update when the call succeeded
(i.e., no exception was thrown); reference functions: getStripeCustomerCountry
and syncStripeCustomerCountry and the supabaseAdmin update block.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2242b0ce-299d-4431-9959-2c1d59130821
📒 Files selected for processing (15)
src/components/admin/AdminMultiLineChart.vuesrc/pages/admin/dashboard/credits.vuesrc/pages/admin/dashboard/users.vuesrc/stores/adminDashboard.tssrc/types/supabase.types.tssupabase/functions/_backend/private/admin_stats.tssupabase/functions/_backend/triggers/stripe_event.tssupabase/functions/_backend/utils/emailClassification.tssupabase/functions/_backend/utils/pg.tssupabase/functions/_backend/utils/postgres_schema.tssupabase/functions/_backend/utils/stripe.tssupabase/functions/_backend/utils/stripe_event.tssupabase/functions/_backend/utils/supabase.types.tssupabase/migrations/20260330141128_stripe_customer_country.sqltests/stripe-country.unit.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b296e13d7d
ℹ️ 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".
|
Addressed the current review feedback in
Validation rerun:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
supabase/functions/_backend/utils/pg.ts (1)
1228-1236: Consider using timestamptz conversion for date extraction to ensure timezone consistency.The current approach extracts dates via
split('T')[0]fordate_series, whilenormalized_usersuses::timestamptzconversion. If the API receives timestamps with non-UTC offsets (e.g.,2024-03-15T02:00:00+05:00), the string split gives2024-03-15, but PostgreSQL's timestamptz conversion would interpret this as2024-03-14in UTC.For consistency with
getAdminOnboardingFunnel(lines 1594-1599), consider using the same pattern:♻️ Suggested alignment with other admin stats functions
- const startDateOnly = start_date.split('T')[0] - const endDateOnly = end_date.split('T')[0] - const personalDomainsSql = sql.join(PERSONAL_EMAIL_DOMAINS.map(domain => sql`${domain}`), sql`, `) const disposableDomainsSql = sql.join(DISPOSABLE_EMAIL_DOMAINS.map(domain => sql`${domain}`), sql`, `) const query = sql` WITH date_series AS ( - SELECT generate_series(${startDateOnly}::date, (${endDateOnly}::date - interval '1 day')::date, interval '1 day')::date AS date + SELECT generate_series( + ${start_date}::timestamptz::date, + (${end_date}::timestamptz::date - interval '1 day')::date, + interval '1 day' + )::date AS date ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_backend/utils/pg.ts` around lines 1228 - 1236, The date extraction using start_date.split('T')[0] and end_date.split('T')[0] is vulnerable to timezone shifts and can diverge from normalized_users which uses ::timestamptz; update startDateOnly/endDateOnly to normalize via PostgreSQL timestamptz conversion (same approach as in getAdminOnboardingFunnel) so the date_series uses UTC-consistent dates, and ensure the generated query's date bounds use the converted timestamps rather than raw string-splits to avoid off-by-one-day errors when timestamps include offsets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@supabase/functions/_backend/utils/pg.ts`:
- Around line 1228-1236: The date extraction using start_date.split('T')[0] and
end_date.split('T')[0] is vulnerable to timezone shifts and can diverge from
normalized_users which uses ::timestamptz; update startDateOnly/endDateOnly to
normalize via PostgreSQL timestamptz conversion (same approach as in
getAdminOnboardingFunnel) so the date_series uses UTC-consistent dates, and
ensure the generated query's date bounds use the converted timestamps rather
than raw string-splits to avoid off-by-one-day errors when timestamps include
offsets.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6080ad4f-057a-4d2f-b951-26bf81734038
📒 Files selected for processing (6)
messages/en.jsonsrc/pages/admin/dashboard/credits.vuesrc/pages/admin/dashboard/users.vuesupabase/functions/_backend/private/download_link.tssupabase/functions/_backend/utils/pg.tssupabase/functions/_backend/utils/stripe.ts
✅ Files skipped from review due to trivial changes (1)
- messages/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
- src/pages/admin/dashboard/users.vue
- supabase/functions/_backend/utils/stripe.ts
- src/pages/admin/dashboard/credits.vue
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a57583173
ℹ️ 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".
There was a problem hiding this comment.
♻️ Duplicate comments (1)
supabase/functions/_backend/utils/pg.ts (1)
1233-1247:⚠️ Potential issue | 🟠 MajorStop subtracting the email-series upper bound twice.
seriesEndDayis already normalized to the last UTC day that should appear in the trend. Line 1247 subtracts another day in SQL, so a range like[2026-03-01T00:00:00Z, 2026-03-02T00:00:00Z)returns an empty series, and every non-empty range drops its final day.🛠️ Proposed fix
const query = sql` WITH date_series AS ( - SELECT generate_series(${startDateOnly}::date, (${endDateOnly}::date - interval '1 day')::date, interval '1 day')::date AS date + SELECT generate_series(${startDateOnly}::date, ${endDateOnly}::date, interval '1 day')::date AS date ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_backend/utils/pg.ts` around lines 1233 - 1247, The date_series end bound is being decremented twice: you already normalized the upper bound into seriesEndDay/endDateOnly, but the query's generate_series uses "(${endDateOnly}::date - interval '1 day')", which removes the last day again; update the SQL in the date_series CTE (the query variable) to use ${endDateOnly}::date as the second argument (keeping startDateOnly and the interval '1 day' as-is) so the series includes the intended final UTC day.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@supabase/functions/_backend/utils/pg.ts`:
- Around line 1233-1247: The date_series end bound is being decremented twice:
you already normalized the upper bound into seriesEndDay/endDateOnly, but the
query's generate_series uses "(${endDateOnly}::date - interval '1 day')", which
removes the last day again; update the SQL in the date_series CTE (the query
variable) to use ${endDateOnly}::date as the second argument (keeping
startDateOnly and the interval '1 day' as-is) so the series includes the
intended final UTC day.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4e7e2a6c-fd8a-4362-aeaf-f15a2887cda1
📒 Files selected for processing (1)
supabase/functions/_backend/utils/pg.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
supabase/functions/_backend/utils/stripe.ts (1)
267-276: Harden country normalization to only accept valid alpha-2 codes.Current logic can persist truncated/invalid values (e.g.,
"USA"→"US","1@"→"1@"). Prefer strict ISO alpha-2 validation before writing.Proposed refactor
export function normalizeStripeCountryCode(country: string | null | undefined): string | null { if (!country) return null const normalized = country.trim().toUpperCase() - if (!normalized) + if (!normalized) return null - - return normalized.slice(0, 2) + if (!/^[A-Z]{2}$/.test(normalized)) + return null + return normalized }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_backend/utils/stripe.ts` around lines 267 - 276, normalizeStripeCountryCode currently trims and slices inputs which can convert invalid values like "USA" -> "US" or leave non-letters ("1@") unchanged; change it to only accept valid ISO alpha-2 codes by trimming and uppercasing the input, then validating with an alpha-2 check (e.g., match /^[A-Z]{2}$/) and return the two-letter code only if it passes, otherwise return null; update the logic inside normalizeStripeCountryCode to perform that validation and return null for any non-conforming input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@supabase/functions/_backend/utils/pg.ts`:
- Around line 1228-1257: The date bucketing code using start_date/end_date
(startTimestamp, endTimestamp, startDay, endDay, seriesEndDay, endExclusive, and
the query) assumes Z-terminated ISO 8601 strings; add runtime validation to
enforce that contract or move parsing into SQL: either (A) validate start_date
and end_date are non-empty strings matching an ISO 8601 UTC pattern (e.g.
/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z/) and throw a 4xx error if
not, then safely new Date(...) afterwards, or (B) stop creating JS Date objects
and instead pass the raw strings directly into the query and cast them in SQL
(e.g. ${start_date}::timestamptz) so Postgres handles parsing; pick one approach
and apply it consistently to the code that builds the query.
In `@supabase/functions/_backend/utils/stripe.ts`:
- Around line 302-307: Replace the direct Supabase admin update using
supabaseAdmin(...) with a Drizzle/pg client call from getPgClient() or
getDrizzleClient(); specifically, in the code that updates
stripe_info.customer_country using customerId/customerCountry, obtain the DB
client via getPgClient()/getDrizzleClient(), import the stripe_info schema from
utils/postgress_schema.ts, then perform a schema-backed update (WHERE
customer_id = customerId) setting customer_country = customerCountry and
return/select customer_id as needed; ensure error handling mirrors the original
behavior and remove the supabaseAdmin(...) update call.
---
Nitpick comments:
In `@supabase/functions/_backend/utils/stripe.ts`:
- Around line 267-276: normalizeStripeCountryCode currently trims and slices
inputs which can convert invalid values like "USA" -> "US" or leave non-letters
("1@") unchanged; change it to only accept valid ISO alpha-2 codes by trimming
and uppercasing the input, then validating with an alpha-2 check (e.g., match
/^[A-Z]{2}$/) and return the two-letter code only if it passes, otherwise return
null; update the logic inside normalizeStripeCountryCode to perform that
validation and return null for any non-conforming input.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5bc92f50-5aa5-40b3-9139-9c2edd58b36f
📒 Files selected for processing (2)
supabase/functions/_backend/utils/pg.tssupabase/functions/_backend/utils/stripe.ts
There was a problem hiding this comment.
🧹 Nitpick comments (2)
supabase/functions/_backend/utils/stripe.ts (1)
302-315: Avoid double-writingcustomer_countryin webhook flows.Line 302-307 writes
stripe_info.customer_countryhere, andupdateStripeInfoinsupabase/functions/_backend/triggers/stripe_event.ts(Line 448-461) can write it again fromstripeData.data. Consider making this helper compute-only (or return a “write already done” signal) so one code path owns persistence.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_backend/utils/stripe.ts` around lines 302 - 315, The helper currently writes stripe_info.customer_country via supabaseAdmin(c).from('stripe_info').update(...) inside syncStripeCustomerCountry, causing potential double-writes when updateStripeInfo in supabase/functions/_backend/triggers/stripe_event.ts also persists customer_country; change syncStripeCustomerCountry to be compute-only (remove the supabaseAdmin update) or add a boolean flag/return value that signals "persistence done" so only one code path owns writes—update callers (including updateStripeInfo) to either persist the returned customerCountry or respect the flag; keep error logging (cloudlogErr/cloudlog) where appropriate but do not duplicate the DB update in syncStripeCustomerCountry.tests/admin-stats.unit.test.ts (1)
42-54: Use concurrent table tests for this case matrix.This block is independent with no shared mocks or state, so it can safely run as
it.concurrent.each(...)per the test parallelism guideline.♻️ Suggested change
- it.each([ + it.concurrent.each([ ['plain date start', { start_date: '2025-01-01' }], ['plain date end', { end_date: '2025-01-31' }], ['offset datetime start', { start_date: '2025-01-01T01:00:00+01:00' }], ['offset datetime end', { end_date: '2025-01-31T01:00:00+01:00' }], ])('rejects non-UTC ISO datetimes for %s', (_label, body) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/admin-stats.unit.test.ts` around lines 42 - 54, The test matrix using it.each in tests/admin-stats.unit.test.ts should be run concurrently; replace the synchronous it.each with it.concurrent.each for the block that calls adminStatsBodySchema.safeParse with baseBody and the various date bodies (the test labeled 'rejects non-UTC ISO datetimes for %s' that references adminStatsBodySchema and baseBody) so each case executes in parallel since there are no shared mocks or state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@supabase/functions/_backend/utils/stripe.ts`:
- Around line 302-315: The helper currently writes stripe_info.customer_country
via supabaseAdmin(c).from('stripe_info').update(...) inside
syncStripeCustomerCountry, causing potential double-writes when updateStripeInfo
in supabase/functions/_backend/triggers/stripe_event.ts also persists
customer_country; change syncStripeCustomerCountry to be compute-only (remove
the supabaseAdmin update) or add a boolean flag/return value that signals
"persistence done" so only one code path owns writes—update callers (including
updateStripeInfo) to either persist the returned customerCountry or respect the
flag; keep error logging (cloudlogErr/cloudlog) where appropriate but do not
duplicate the DB update in syncStripeCustomerCountry.
In `@tests/admin-stats.unit.test.ts`:
- Around line 42-54: The test matrix using it.each in
tests/admin-stats.unit.test.ts should be run concurrently; replace the
synchronous it.each with it.concurrent.each for the block that calls
adminStatsBodySchema.safeParse with baseBody and the various date bodies (the
test labeled 'rejects non-UTC ISO datetimes for %s' that references
adminStatsBodySchema and baseBody) so each case executes in parallel since there
are no shared mocks or state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f52b1a99-c105-4572-8b0e-acfeefe2365d
📒 Files selected for processing (5)
AGENTS.mdsupabase/functions/_backend/private/admin_stats.tssupabase/functions/_backend/utils/stripe.tstests/admin-stats.unit.test.tstests/stripe-country.unit.test.ts
✅ Files skipped from review due to trivial changes (2)
- AGENTS.md
- tests/stripe-country.unit.test.ts
|



Summary (AI generated)
stripe_info.customer_countryand sync it from Stripe customer webhook eventsMotivation (AI generated)
The admin dashboard was missing visibility into credit sales, credit usage mix, and email quality signals. We also needed a reliable database-level country field sourced from Stripe so future customer geography reporting can run from our own data instead of live Stripe lookups.
Business Impact (AI generated)
This gives the team direct visibility into paid credit behavior, revenue composition, signup quality, and customer geography. That improves decision-making around monetization, fraud/disposable-email monitoring, and regional customer analysis without adding more manual Stripe investigation.
Test Plan (AI generated)
bunx eslint --no-warn-ignored src/components/admin/AdminMultiLineChart.vue src/pages/admin/dashboard/credits.vue src/pages/admin/dashboard/users.vue src/stores/adminDashboard.ts supabase/functions/_backend/private/admin_stats.ts supabase/functions/_backend/triggers/stripe_event.ts supabase/functions/_backend/utils/pg.ts supabase/functions/_backend/utils/postgres_schema.ts supabase/functions/_backend/utils/stripe.ts supabase/functions/_backend/utils/stripe_event.ts supabase/functions/_backend/utils/emailClassification.ts tests/stripe-country.unit.test.tsbun typecheckbunx vitest run tests/stripe-country.unit.test.ts tests/stripe-event-paid-at.unit.test.ts tests/admin-stats.unit.test.tsGenerated with AI
Summary by CodeRabbit
New Features
Enhancements
Migrations
Tests
Localization