Skip to content

Conversation

@bassgeta
Copy link
Contributor

@bassgeta bassgeta commented Oct 16, 2025

Problem

The multi currency display was a bit ugly, we should use conversion values from our API.

Solution

Alas, unpaid invoices do not have that data so I had to create some helpers to pull that value out :)

Changes

  • new helpers/conversion.ts file which does the following:
  1. Pass it an array of requests/subscription payments/recurring payment installments
  2. It converts it to a generic paymentItem object
  3. Then it sums up all payments which are stablecoins or USD with payments that have conversion info
  4. It also returns a boolean if we have payments that are not in USD and do not have conversion info
  • adapted all components using the MultiCurrencyStatCard to now use these helpers and display an asterisk note under the ordinary StatCard if we have payments not in stablecoins without conversion

How to test it

Probably just open up easy invoice and go over the following pages:

  • /dashboard/pay
  • /dashboard/get-paid
  • /dashboard/subscriptions
  • /subscription-plans#subscribers
  • /subscription-plans#payments

and verify that:

  1. All stablecoins and USD requests show up
  2. Existing invoices in ETH/whatever make the asterisk pop up
  3. If you pay an invoice with an invoiceCurrency of ETH, that total should show up in the stat card.

Resolves #143

Summary by CodeRabbit

  • New Features

    • Dashboards and reports now show consolidated USD totals for invoices, subscriptions, and revenue, with a note when non-USD items are excluded.
    • Conversion details are now surfaced on recurring payments, subscription payments, and client payment records where available.
    • Simplified payment lists and tables use single USD totals with contextual non-USD footnotes.
  • Bug Fixes

    • Improved consistency and accuracy of currency totals across dashboard cards and tables.

…helpers to get USD amount for requests, subscription payments and recurring payments
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

Threads ConversionInfo through webhook flows, introduces typed ClientPaymentBody and RecurringPaymentInstallment, adds conversion utilities to consolidate USD values, removes MultiCurrencyStatCard and legacy currency helpers, updates dashboard components to display consolidated USD totals with non-USD indicators, and adds DB migration for conversionInfo fields.

Changes

Cohort / File(s) Summary
Webhook & Types
src/app/api/webhook/route.ts, src/lib/types/index.ts
Introduced ClientPaymentBody, getClientPaymentBody, getConversionInfo, replaced updateRequestStatus with updateRequest(requestId, Partial<RequestModel>), and threaded ConversionInfo through webhook flows.
DB Schema & Migration
src/server/db/schema.ts, drizzle/0011_curvy_baron_strucker.sql, drizzle/meta/0011_snapshot.json, drizzle/meta/_journal.json
Added conversionInfo json column to requests and client payments, added RecurringPaymentInstallment type, updated recurring payments payments typed array, and added journal/snapshot metadata for migration.
Conversion Utilities
src/lib/helpers/conversion.ts (new), src/lib/helpers/currency.ts (removed)
Added unified PaymentItem type, converters and consolidators (consolidateRequestUsdValues, consolidateSubscriptionPaymentUsdValues, consolidateRecurringPaymentUsdValues, isStablecoin); removed old per-currency helpers (calculateTotalsByCurrency, formatCurrencyTotals).
Dashboard — Invoices
src/components/dashboard/invoices-received.tsx, src/components/dashboard/invoices-sent.tsx
Replaced per-currency totals and MultiCurrencyStatCard with consolidated USD totals via consolidateRequestUsdValues; show USD StatCard and conditional non-USD note.
Dashboard — Subscriptions & Plans
src/components/dashboard/subscriptions.tsx, src/components/subscription-plans/blocks/payments-table.tsx, src/components/subscription-plans/blocks/subscribers-table.tsx
Replaced multi-currency flow with consolidation helpers (consolidateRecurringPaymentUsdValues, consolidateSubscriptionPaymentUsdValues), swapped MultiCurrencyStatCard for StatCard, aggregated USD totals and added conditional non-USD notes; minor UI/icon adjustments.
Component Removals & Prop Types
src/components/multi-currency-stat-card.tsx (removed), src/components/view-recurring-payments/blocks/completed-payments.tsx
Removed MultiCurrencyStatCard and its props; changed CompletedPayments prop type to RecurringPaymentInstallment[].
Server Routers
src/server/routers/subscription-plan.ts
getAllPayments now includes conversionInfo on returned SubscriptionPayment entries.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Webhook POST
    participant Conv as getConversionInfo()
    participant Builder as getClientPaymentBody()
    participant Logic as addClientPayment / addPaymentToRecurringPayment
    participant DB as Database

    Client->>Conv: parse conversionInfo from webhookBody
    Conv-->>Client: ConversionInfo | null

    alt client payment
        Client->>Builder: getClientPaymentBody(webhookBody, conversionInfo)
        Builder-->>Client: ClientPaymentBody
        Client->>Logic: addClientPayment(ClientPaymentBody, clientId)
        Logic->>DB: insert client payment (with conversionInfo)
        DB-->>Logic: ok
    end

    alt recurring payment
        Client->>Logic: addPaymentToRecurringPayment(externalPaymentId, RecurringPaymentInstallment{..., conversionInfo})
        Logic->>DB: append installment (typed)
        DB-->>Logic: ok
    end

    Client->>DB: updateRequest(requestId, { ...requestData })
    DB-->>Client: updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rodrigopavezi
  • aimensahnoun
  • MantisClone

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: use webhook conversion info for all payments and requests" directly aligns with the primary objective of this changeset. The changes introduce conversion information handling across the webhook processing flow, database schema, and multiple dashboard components. The title is concise, specific, and clearly communicates that the main change involves leveraging webhook-provided conversion data for payments and requests. It avoids vague language and provides sufficient clarity for a teammate to understand the core modification when scanning the PR history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/143-payment-conversion-info

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf5029b and 69a1455.

📒 Files selected for processing (3)
  • drizzle/0011_curvy_baron_strucker.sql (1 hunks)
  • drizzle/meta/0011_snapshot.json (1 hunks)
  • drizzle/meta/_journal.json (1 hunks)
🔇 Additional comments (3)
drizzle/0011_curvy_baron_strucker.sql (1)

1-2: Migration syntax and design are correct.

The two new conversionInfo columns are properly added as nullable JSON columns. This design allows the PR's conversion helpers to work with both legacy records (which lack conversion data) and new records (which include it). The absence of a NOT NULL constraint is appropriate for backwards compatibility.

drizzle/meta/_journal.json (1)

82-88: Journal entry is correctly sequenced and formatted.

The migration metadata is properly ordered (idx 11 after idx 10) with a reasonable timestamp progression. All required fields (idx, version, when, tag, breakpoints) are present and follow the established pattern.

drizzle/meta/0011_snapshot.json (1)

90-95: Schema snapshot correctly reflects the new conversionInfo columns.

Both tables (easyinvoice_client_payment and easyinvoice_request) now include the conversionInfo JSON column with nullable properties, matching the migration. The snapshot is consistent with the PR's objective to propagate conversion metadata through payments and requests.

Ensure the TypeScript Drizzle schema (e.g., src/server/db/schema.ts) defines the ConversionInfo type and applies .$type<ConversionInfo>() to these columns for compile-time type safety. This provides the type safety benefits that Drizzle JSON columns require.

Also applies to: 911-916


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

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/routers/subscription-plan.ts (1)

185-203: Coalesce conversionInfo to null to match the type contract

Guard against undefined coming from DB/joins.

-                conversionInfo: payment.conversionInfo,
+                conversionInfo: payment.conversionInfo ?? null,
🧹 Nitpick comments (1)
src/lib/types/index.ts (1)

57-64: Add a shared schema for ConversionInfo

Consider a Zod schema (server-side) for ConversionInfo to validate webhook payloads/DB reads and keep the shape consistent across boundaries.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88e4810 and 9602833.

📒 Files selected for processing (13)
  • src/app/api/webhook/route.ts (8 hunks)
  • src/components/dashboard/invoices-received.tsx (3 hunks)
  • src/components/dashboard/invoices-sent.tsx (3 hunks)
  • src/components/dashboard/subscriptions.tsx (3 hunks)
  • src/components/multi-currency-stat-card.tsx (0 hunks)
  • src/components/subscription-plans/blocks/payments-table.tsx (5 hunks)
  • src/components/subscription-plans/blocks/subscribers-table.tsx (6 hunks)
  • src/components/view-recurring-payments/blocks/completed-payments.tsx (1 hunks)
  • src/lib/helpers/conversion.ts (1 hunks)
  • src/lib/helpers/currency.ts (0 hunks)
  • src/lib/types/index.ts (1 hunks)
  • src/server/db/schema.ts (5 hunks)
  • src/server/routers/subscription-plan.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • src/lib/helpers/currency.ts
  • src/components/multi-currency-stat-card.tsx
🧰 Additional context used
🧬 Code graph analysis (10)
src/components/subscription-plans/blocks/payments-table.tsx (2)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/lib/helpers/conversion.ts (1)
  • consolidateSubscriptionPaymentUsdValues (82-90)
src/server/db/schema.ts (1)
src/lib/types/index.ts (1)
  • ConversionInfo (57-64)
src/components/dashboard/invoices-sent.tsx (2)
src/lib/helpers/conversion.ts (1)
  • consolidateRequestUsdValues (74-80)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/lib/types/index.ts (1)
src/server/routers/ecommerce.ts (1)
  • ecommerceRouter (14-182)
src/lib/helpers/conversion.ts (2)
src/lib/types/index.ts (2)
  • ConversionInfo (57-64)
  • SubscriptionPayment (37-51)
src/server/db/schema.ts (2)
  • Request (486-486)
  • RecurringPaymentInstallment (88-93)
src/components/subscription-plans/blocks/subscribers-table.tsx (3)
src/lib/types/index.ts (1)
  • SubscriptionWithDetails (29-35)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/lib/helpers/conversion.ts (1)
  • consolidateRecurringPaymentUsdValues (92-106)
src/components/dashboard/invoices-received.tsx (2)
src/lib/helpers/conversion.ts (1)
  • consolidateRequestUsdValues (74-80)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/components/view-recurring-payments/blocks/completed-payments.tsx (1)
src/server/db/schema.ts (1)
  • RecurringPaymentInstallment (88-93)
src/components/dashboard/subscriptions.tsx (3)
src/lib/types/index.ts (1)
  • SubscriptionWithDetails (29-35)
src/lib/helpers/conversion.ts (1)
  • consolidateRecurringPaymentUsdValues (92-106)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/app/api/webhook/route.ts (2)
src/server/db/schema.ts (4)
  • ClientPayment (497-497)
  • ecommerceClientTable (305-331)
  • requestTable (188-226)
  • RecurringPaymentInstallment (88-93)
src/lib/types/index.ts (1)
  • ConversionInfo (57-64)
🔇 Additional comments (4)
src/lib/types/index.ts (1)

50-51: Contract: prefer null over undefined for conversionInfo

Good addition. Ensure all mappers/serializers coalesce to null when no data so the shape matches the declared type.

src/components/subscription-plans/blocks/subscribers-table.tsx (2)

183-200: USD consolidation per subscription — LGTM

Aggregation via consolidateRecurringPaymentUsdValues over active subscribers looks correct.


264-265: EmptyState icon change — LGTM

The switch to Users fits the context.

src/components/subscription-plans/blocks/payments-table.tsx (1)

151-153: USD consolidation on filtered set — LGTM

Using consolidateSubscriptionPaymentUsdValues over the filtered list is correct.

Copy link
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: 1

🧹 Nitpick comments (6)
src/components/dashboard/invoices-sent.tsx (2)

79-85: Consider improving accessibility of the asterisk note.

The asterisk note lacks semantic structure and may not be announced properly by screen readers. Consider using an aria-label or aria-describedby to associate the note with the stat card.

-          <StatCard
+          <StatCard
+            aria-describedby={hasNonUsdValues ? "usd-conversion-note" : undefined}
             title="Total Payments"
             value={`$${Number(totalInUsd).toLocaleString()}`}
             icon={<DollarSign className="h-4 w-4 text-muted-foreground" />}
           />
           {hasNonUsdValues && (
-            <p className="text-xs text-muted-foreground text-center mt-1">
+            <p id="usd-conversion-note" className="text-xs text-muted-foreground text-center mt-1">
               * Excludes non-USD invoices without conversion info
             </p>
           )}

76-76: Standardize USD formatting with explicit locale and precision
Convert the string totalInUsd to a number and use .toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) for consistent USD formatting:

- value={`$${Number(totalInUsd).toLocaleString()}`}
+ value={`$${Number(totalInUsd).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
src/components/subscription-plans/blocks/subscribers-table.tsx (4)

211-215: Format USD with Intl.NumberFormat (avoid manual '$' + toLocaleString)

Ensures consistent currency formatting and 2 decimal places.

-            value={`$${totalRevenue.toLocaleString()}`}
+            value={new Intl.NumberFormat(undefined, {
+              style: "currency",
+              currency: "USD",
+              minimumFractionDigits: 2,
+              maximumFractionDigits: 2,
+            }).format(totalRevenue)}

210-223: Prevent footnote overlap; avoid absolute positioning

Current absolute -bottom-5 can overlap following content. Keep it in normal flow with margin. Copy reads well; retain as-is.

-        <div className="relative">
+        <div>
           <StatCard
             title="Total Revenue"
-            value={`$${totalRevenue.toLocaleString()}`}
+            value={new Intl.NumberFormat(undefined, {
+              style: "currency",
+              currency: "USD",
+              minimumFractionDigits: 2,
+              maximumFractionDigits: 2,
+            }).format(totalRevenue)}
             icon={<DollarSign className="h-4 w-4 text-muted-foreground" />}
           />
-          {hasNonUsdValues && (
-            <div className="absolute -bottom-5 left-0 right-0 text-center">
-              <p className="text-xs text-muted-foreground">
-                * Excludes non-USD subscriptions without conversion info
-              </p>
-            </div>
-          )}
+          {hasNonUsdValues && (
+            <p className="mt-1 text-center text-xs text-muted-foreground">
+              * Excludes non-USD subscriptions without conversion info
+            </p>
+          )}
         </div>

205-209: Optional: icon semantics

Consider Users for “Active Subscriptions” to better match subscriber count.

-        <StatCard
+        <StatCard
           title="Active Subscriptions"
           value={`${activeSubscribers.length}`}
-          icon={<CreditCard className="h-4 w-4 text-muted-foreground" />}
+          icon={<Users className="h-4 w-4 text-muted-foreground" />}
         />

183-200: Harden USD total accumulation (guard parseFloat + NaN check)
Replace Number(totalInUsd) with parseFloat and guard against NaN; simplify hasNonUsdValues assignment:

- totalRevenue += Number(totalInUsd);
- if (subHasNonUsdValues) {
-   hasNonUsdValues = true;
- }
+ const amount = parseFloat(totalInUsd);
+ if (!Number.isNaN(amount)) {
+   totalRevenue += amount;
+ }
+ if (subHasNonUsdValues) {
+   hasNonUsdValues = true;
+ }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9602833 and bf5029b.

📒 Files selected for processing (7)
  • drizzle/meta/_journal.json (1 hunks)
  • src/components/dashboard/invoices-received.tsx (3 hunks)
  • src/components/dashboard/invoices-sent.tsx (3 hunks)
  • src/components/subscription-plans/blocks/payments-table.tsx (5 hunks)
  • src/components/subscription-plans/blocks/subscribers-table.tsx (6 hunks)
  • src/lib/helpers/conversion.ts (1 hunks)
  • src/server/routers/subscription-plan.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • drizzle/meta/_journal.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/server/routers/subscription-plan.ts
  • src/components/dashboard/invoices-received.tsx
  • src/lib/helpers/conversion.ts
  • src/components/subscription-plans/blocks/payments-table.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/dashboard/invoices-sent.tsx (2)
src/lib/helpers/conversion.ts (1)
  • consolidateRequestUsdValues (77-83)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/components/subscription-plans/blocks/subscribers-table.tsx (3)
src/lib/types/index.ts (1)
  • SubscriptionWithDetails (29-35)
src/components/stat-card.tsx (1)
  • StatCard (9-21)
src/lib/helpers/conversion.ts (1)
  • consolidateRecurringPaymentUsdValues (95-109)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (3)
src/components/dashboard/invoices-sent.tsx (2)

12-12: LGTM! Clean import of the new conversion helper.

The import is appropriate for the new USD consolidation functionality.


53-55: Good defensive handling with empty array fallback.

The empty array fallback (invoices || []) prevents potential runtime errors when invoices are undefined.

src/components/subscription-plans/blocks/subscribers-table.tsx (1)

264-265: EmptyState icon swap to Users — LGTM

Matches the subscribers context.

@bassgeta bassgeta force-pushed the feat/143-payment-conversion-info branch from bf5029b to 69a1455 Compare October 17, 2025 07:50
Copy link
Member

@MantisClone MantisClone left a comment

Choose a reason for hiding this comment

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

LGTM!

Clean implementation of USD consolidation using webhook conversion data. All critical aspects verified:

Security: HMAC signature validation, transaction-safe DB ops, null-safe handling
Architecture: Display-only formatting with Number() — all transaction amounts remain strings/BigNumber
Quality: All CodeRabbit issues resolved, backward-compatible migration, no technical debt

Ready to merge.

@rodrigopavezi
Copy link
Member

Pull Request Review

Summary

This PR adds conversionInfo JSON columns to two database tables to track currency conversion data.

Changes

  • Adds nullable conversionInfo JSON column to easyinvoice_client_payment table
  • Adds nullable conversionInfo JSON column to easyinvoice_request table

Review Findings

✅ Good

  • Safe additive migration (no breaking changes)
  • Proper nullable columns since conversion info is optional
  • Follows existing database naming conventions
  • Uses PostgreSQL JSON type appropriately

⚠️ Issues

  • Missing input validation: The webhook handler doesn't validate conversion data structure before storing
  • No migration comments: Should document what these columns are for

Recommendation

Approve with minor fixes

Add input validation in the webhook handler (src/app/api/webhook/route.ts) to validate the conversion info structure before database insertion.

Risk Level

🟡 Low - Migration is safe, main concern is data quality from unvalidated inputs.

@bassgeta bassgeta merged commit 839c83d into main Oct 27, 2025
5 checks passed
@bassgeta bassgeta deleted the feat/143-payment-conversion-info branch October 27, 2025 11:54
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.

EasyInvoice - Consolidate multiple currency amounts to a single USD amount

4 participants