Skip to content

[codex] Add credit analytics and Stripe customer country sync#1875

Merged
riderx merged 9 commits intomainfrom
codex/admin-credit-analytics-country-sync
Mar 30, 2026
Merged

[codex] Add credit analytics and Stripe customer country sync#1875
riderx merged 9 commits intomainfrom
codex/admin-credit-analytics-country-sync

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 30, 2026

Summary (AI generated)

  • add admin credit analytics charts for credits bought and used by day and month
  • add monthly revenue visualization that combines MRR with credit sales
  • add admin email classification analytics for professional, personal, and disposable domains
  • add stripe_info.customer_country and sync it from Stripe customer webhook events
  • add unit coverage for the new Stripe country sync helpers and event routing

Motivation (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.ts
  • bun typecheck
  • bunx vitest run tests/stripe-country.unit.test.ts tests/stripe-event-paid-at.unit.test.ts tests/admin-stats.unit.test.ts

Generated with AI

Summary by CodeRabbit

  • New Features

    • Credits Analytics dashboard (daily/monthly credits + revenue) and Email Type Breakdown analytics; admin dashboard loads and refreshes these reports.
    • Automatic syncing of customer country from Stripe profiles.
  • Enhancements

    • Improved chart formatting with day/month granularity, localized numeric formatting, and configurable value prefix/suffix.
    • Email-domain classification for professional/personal/disposable reporting.
  • Migrations

    • Added customer_country column to billing data.
  • Tests

    • Unit tests for Stripe country sync and event handling.
  • Localization

    • Added English UI strings for new admin analytics.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Chart Component
src/components/admin/AdminMultiLineChart.vue
Added props dateGranularity, valuePrefix, valueSuffix; monthly x-axis formatting and unified numeric formatting for tooltips and y-axis ticks.
Admin Dashboard Pages
src/pages/admin/dashboard/credits.vue, src/pages/admin/dashboard/users.vue
Added Credits Analytics and Email Type Breakdown UIs, loaders, series builders, request-sequencing guard, watchers, and lifecycle wiring to load analytics.
Admin Store Types
src/stores/adminDashboard.ts
Extended MetricCategory to include 'email_type_breakdown'.
Email Classification Utils
supabase/functions/_backend/utils/emailClassification.ts
New module: EmailType union, domain lists, extractEmailDomain, classifyEmailDomain, classifyEmailAddress.
Analytics Backend & PG Utils
supabase/functions/_backend/private/admin_stats.ts, supabase/functions/_backend/utils/pg.ts
Accepted metric_category: 'email_type_breakdown'; added getAdminEmailTypeBreakdown; extended AdminGlobalStatsTrend and queries to include credits_bought/credits_consumed.
Stripe country sync & webhook
supabase/functions/_backend/utils/stripe.ts, supabase/functions/_backend/triggers/stripe_event.ts, supabase/functions/_backend/utils/stripe_event.ts
Added country normalization, retrieval, syncStripeCustomerCountry; early handling for customer.created/customer.updated and extraction of customer_id/status.
DB schema / Types / Migration
supabase/functions/_backend/utils/postgres_schema.ts, supabase/migrations/20260330141128_stripe_customer_country.sql, src/types/supabase.types.ts, supabase/functions/_backend/utils/supabase.types.ts
Added customer_country VARCHAR(2) to stripe_info via migration and updated TypeScript types for Row/Insert/Update.
Tests
tests/stripe-country.unit.test.ts, tests/admin-stats.unit.test.ts
Added Vitest coverage for country normalization and event extraction; tightened admin_stats date validation tests to require ISO UTC datetime.
I18n & Messages
messages/en.json
Added English strings for credits analytics and email-type breakdown UI.
Other backend tweak
supabase/functions/_backend/private/download_link.ts
Manifest lookup changed to query by .eq('app_version_id', body.id) in isManifest branch.
Docs
AGENTS.md
Added guidance on when to use DB clients vs supabaseAdmin(c) for internal writes.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hop through charts both bright and sunny,
I count bought credits and emails funny,
I fetch country codes from Stripe with care,
I sort domains into bins with flair,
Then tap the dashboard—metrics bloom everywhere!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main additions: credit analytics functionality and Stripe customer country synchronization, both key objectives mentioned in the PR summary.
Description check ✅ Passed The PR description includes a summary, motivation, business impact, and detailed test plan with specific commands executed. However, it lacks the formal structure and sections specified in the repository's description template (Summary, Test plan, Screenshots, Checklist).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/admin-credit-analytics-country-sync

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Mar 30, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/admin-credit-analytics-country-sync (cabc1c6) with main (f53ec7c)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review March 30, 2026 18:17
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6bfd4f7 and 8a8fbbf.

📒 Files selected for processing (15)
  • src/components/admin/AdminMultiLineChart.vue
  • src/pages/admin/dashboard/credits.vue
  • src/pages/admin/dashboard/users.vue
  • src/stores/adminDashboard.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/private/admin_stats.ts
  • supabase/functions/_backend/triggers/stripe_event.ts
  • supabase/functions/_backend/utils/emailClassification.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/supabase.types.ts
  • supabase/migrations/20260330141128_stripe_customer_country.sql
  • tests/stripe-country.unit.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Copy link
Copy Markdown
Member Author

riderx commented Mar 30, 2026

Addressed the current review feedback in 7a5758317.

  • guarded the new credits analytics loader against stale out-of-order responses
  • moved the new credits/email analytics copy and chart labels to i18n keys in messages/en.json
  • normalized the email breakdown query to whole UTC day buckets so both day labels and totals use consistent boundaries
  • stopped customer_country from being cleared on transient Stripe lookup failures
  • fixed the manifest lookup in download_link.ts to use app_version_id, which was also blocking bun typecheck

Validation rerun:

  • bunx eslint --no-warn-ignored src/pages/admin/dashboard/credits.vue src/pages/admin/dashboard/users.vue supabase/functions/_backend/utils/pg.ts supabase/functions/_backend/utils/stripe.ts supabase/functions/_backend/private/download_link.ts messages/en.json tests/stripe-country.unit.test.ts
  • bun typecheck
  • bunx vitest run tests/admin-stats.unit.test.ts tests/stripe-country.unit.test.ts tests/stripe-event-paid-at.unit.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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] for date_series, while normalized_users uses ::timestamptz conversion. If the API receives timestamps with non-UTC offsets (e.g., 2024-03-15T02:00:00+05:00), the string split gives 2024-03-15, but PostgreSQL's timestamptz conversion would interpret this as 2024-03-14 in 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8a8fbbf and 094c7cf.

📒 Files selected for processing (6)
  • messages/en.json
  • src/pages/admin/dashboard/credits.vue
  • src/pages/admin/dashboard/users.vue
  • supabase/functions/_backend/private/download_link.ts
  • supabase/functions/_backend/utils/pg.ts
  • supabase/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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
supabase/functions/_backend/utils/pg.ts (1)

1233-1247: ⚠️ Potential issue | 🟠 Major

Stop subtracting the email-series upper bound twice.

seriesEndDay is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 094c7cf and 7a57583.

📒 Files selected for processing (1)
  • supabase/functions/_backend/utils/pg.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7a57583 and 5b7c836.

📒 Files selected for processing (2)
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/stripe.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
supabase/functions/_backend/utils/stripe.ts (1)

302-315: Avoid double-writing customer_country in webhook flows.

Line 302-307 writes stripe_info.customer_country here, and updateStripeInfo in supabase/functions/_backend/triggers/stripe_event.ts (Line 448-461) can write it again from stripeData.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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b7c836 and c447361.

📒 Files selected for processing (5)
  • AGENTS.md
  • supabase/functions/_backend/private/admin_stats.ts
  • supabase/functions/_backend/utils/stripe.ts
  • tests/admin-stats.unit.test.ts
  • tests/stripe-country.unit.test.ts
✅ Files skipped from review due to trivial changes (2)
  • AGENTS.md
  • tests/stripe-country.unit.test.ts

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit a8673fa into main Mar 30, 2026
17 of 18 checks passed
@riderx riderx deleted the codex/admin-credit-analytics-country-sync branch March 30, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant