-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Refactor currencyFormatter #2801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughStandardized currency display across the app by simplifying currencyFormatter usage, often removing explicit fraction options. Updated the core currency-formatter utility’s signature and defaults. Minor signature tweaks in Slack templates. No data flow or structural changes; updates are presentation-focused. Changes
Sequence Diagram(s)Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx (1)
152-159: Treat 0 amounts as $0.00, not "-"Truthiness check hides legitimate zero values. Prefer a nullish check so 0 renders as $0.00.
- cell: ({ row }) => - row.original.amount - ? currencyFormatter(row.original.amount / 100) - : "-", + cell: ({ row }) => + row.original.amount != null + ? currencyFormatter(row.original.amount / 100) + : "-",apps/web/lib/integrations/slack/transform.ts (2)
285-292: Fix lint error: unexpected empty object patternBiome flags the empty destructuring. Accept but ignore the param using an underscore.
-const bountyCreatedTemplate = ({} /* data */ : { data: unknown }) => ({ +const bountyCreatedTemplate = (_: { data: unknown }) => ({
294-301: Fix lint error: unexpected empty object patternSame issue as above.
-const bountyUpdatedTemplate = ({} /* data */ : { data: unknown }) => ({ +const bountyUpdatedTemplate = (_: { data: unknown }) => ({
🧹 Nitpick comments (27)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (1)
428-431: Match tooltip with header number formatting (strip .00 for integers).Top “Earnings” uses NumberFlow with trailingZeroDisplay: "stripIfInteger"; align the tooltip for consistency.
- {currency - ? currencyFormatter(d.values.main) + {currency + ? currencyFormatter(d.values.main, { + trailingZeroDisplay: "stripIfInteger", + }) : nFormatter(d.values.main)}apps/web/ui/partners/overview/blocks/partners-block.tsx (1)
78-78: Optional: strip .00 in compact list rows.Slightly tighter look for integer amounts:
- <span>{currencyFormatter(partner.saleAmount / 100)}</span> + <span> + {currencyFormatter(partner.saleAmount / 100, { + trailingZeroDisplay: "stripIfInteger", + })} + </span>packages/email/src/templates/partner-payout-failed.tsx (1)
44-44: Confirm currency and format failureFee consistently
- This assumes USD defaults. If partner payouts can be non-USD, add currency to props and pass it to currencyFormatter.
- Also format the failure fee with currencyFormatter for consistency (Line 79 currently interpolates a raw number).
Example (outside this hunk):
<span className="font-semibold text-neutral-800"> {currencyFormatter((payout.failureFee ?? 0) / 10 ** 2)} </span>packages/email/src/templates/partner-paypal-payout-failed.tsx (1)
43-43: Pass explicit currency if PayPal payouts aren’t always USDIf payouts may be non-USD, surface currency on the template props and pass it to currencyFormatter to avoid USD-only rendering.
apps/web/ui/partners/overview/blocks/countries-block.tsx (1)
83-83: Verify currency semantics for saleAmountThis assumes USD (defaults) and 2-decimal scaling. If analytics can be multi-currency, pass currency and convert from minor units by exponent.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (2)
256-269: Prefer raw numbers for accessorFn; format in cell (future-proof sorting/aggregation)Returning formatted strings from
accessorFncan hinder numeric ops later. Keep the accessor numeric and format incell.- { - id: "saleAmount", - header: "Revenue", - accessorFn: (d) => currencyFormatter(d.saleAmount / 100), - }, + { + id: "saleAmount", + header: "Revenue", + accessorFn: (d) => d.saleAmount, + cell: ({ row }) => currencyFormatter(row.original.saleAmount / 100), + }, { id: "totalCommissions", header: "Commissions", - accessorFn: (d) => currencyFormatter(d.totalCommissions / 100), + accessorFn: (d) => d.totalCommissions, + cell: ({ row }) => + currencyFormatter(row.original.totalCommissions / 100), }, { id: "netRevenue", header: "Net Revenue", - accessorFn: (d) => currencyFormatter(d.netRevenue / 100), + accessorFn: (d) => d.netRevenue, + cell: ({ row }) => currencyFormatter(row.original.netRevenue / 100), },
301-310: Sortable column id mismatch: use "totalCommissions" not "commissions"This prevents sorting that column.
sortableColumns: [ "createdAt", "clicks", "leads", "conversions", "sales", "saleAmount", - "commissions", + "totalCommissions", "netRevenue", ],apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/client.tsx (1)
127-128: Prefer getValue over row.original in cells.Keeps display concerns decoupled from row shape and leverages the column accessor.
- cell: ({ row }) => currencyFormatter(row.original.commissions / 100), + cell: ({ getValue }) => currencyFormatter(Number(getValue()) / 100),Note: If you want display parity with the NumberFlow total (which strips .00 for integers), consider passing
{ trailingZeroDisplay: "stripIfInteger" }to currencyFormatter in this view.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (2)
82-82: Avoid hard-coded "$0.00"; defer zero/negative handling to the formatter.Using a literal breaks consistency if defaults change (currency, locale) and misreports negatives. Always call the formatter for 0/negatives.
- <>{amount > 0 ? currencyFormatter(amount / 100) : "$0.00"}</> + {currencyFormatter(((amount ?? 0)) / 100)}
26-33: Loading vs. error UX is inconsistent.
isLoadingcollapses errors into the skeleton state, making the innererror ? "-" : ...branch effectively dead on error. Consider keeping error distinct.- const isLoading = amount === undefined || error; + const isLoading = amount === undefined;This will render "-" on errors as intended.
Also applies to: 36-36, 79-86
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx (1)
143-147: Confirm zero-reward display semantics.
commission?.earnings ? ... : "-"hides a $0.00 reward with "-". If you want explicit zeros for clarity, format 0 instead.- value: commission?.earnings - ? currencyFormatter(commission.earnings / 100) - : "-", + value: + commission?.earnings !== undefined + ? currencyFormatter((commission?.earnings ?? 0) / 100) + : "-",apps/web/ui/links/link-analytics-badge.tsx (2)
107-111: Remove the ts-ignore by widening the formatter’s option type.Avoid suppressing TS for
trailingZeroDisplay. Update the sharedCurrencyFormatterOptionsto include it, then drop the ignore here.Outside this file (in @dub/utils):
// packages/utils/src/functions/currency-formatter.ts export type CurrencyFormatterOptions = Intl.NumberFormatOptions & { trailingZeroDisplay?: "auto" | "stripIfInteger"; };After that, this call compiles without the ts-ignore.
171-176: Duplicate config — factor into a tiny helper.You pass the same
{ trailingZeroDisplay: "stripIfInteger" }twice. A localformatSaleshelper improves readability and avoids drift.+ const formatSales = (cents: number) => + currencyFormatter(cents / 100, { trailingZeroDisplay: "stripIfInteger" }); ... - ? currencyFormatter(value / 100, { - trailingZeroDisplay: "stripIfInteger", - }) + ? formatSales(value) ... - ? currencyFormatter(value / 100, { - // @ts-ignore – trailingZeroDisplay is a valid option but TS is outdated - trailingZeroDisplay: "stripIfInteger", - }) + ? formatSales(value)apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/client.tsx (1)
155-167: Optional: match NumberFlow’s no-decimal integersTop totals use
trailingZeroDisplay: "stripIfInteger"(Lines 219-223), but table cells always show two decimals via the default formatter. If you want visual consistency in this view, pass the same option here.- cell: ({ row }) => currencyFormatter(row.original.amount / 100), + cell: ({ row }) => + currencyFormatter(row.original.amount / 100, { + trailingZeroDisplay: "stripIfInteger", + }), ... - cell: ({ row }) => currencyFormatter(row.original.fee / 100), + cell: ({ row }) => + currencyFormatter(row.original.fee / 100, { + trailingZeroDisplay: "stripIfInteger", + }), ... - cell: ({ row }) => currencyFormatter(row.original.total / 100), + cell: ({ row }) => + currencyFormatter(row.original.total / 100, { + trailingZeroDisplay: "stripIfInteger", + }),Additionally (outside this hunk), consider applying the same option to tooltip and Y-axis values (Lines 272 and 283) to keep all numbers in this page consistent.
// Line 272 currencyFormatter(d.values.value / 100, { trailingZeroDisplay: "stripIfInteger" }) // Line 283 value => currencyFormatter(value / 100, { trailingZeroDisplay: "stripIfInteger" })apps/web/ui/partners/rewards/rewards-logic.tsx (1)
257-259: stripIfInteger likely ineffective with formatter’s fixed 2 fraction digits; pass digits here if you want $100 instead of $100.00.currencyFormatter defaults to minimumFractionDigits: 2 and maximumFractionDigits: 2. Without overriding them, trailingZeroDisplay: "stripIfInteger" won’t hide .00. If the intent is to trim zeros in this badge, override digits locally:
- : currencyFormatter(Number(value), { - trailingZeroDisplay: "stripIfInteger", - }); + : currencyFormatter(Number(value), { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + trailingZeroDisplay: "stripIfInteger", + });apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-composite-chart.tsx (1)
182-183: Optional: align tooltip formatting with header (no .00 for integers).Header uses NumberFlow with trailingZeroDisplay: "stripIfInteger". Consider matching it in tooltip rows for consistency:
- {currencyFormatter((d.values.total || 0) / 100)} + {currencyFormatter((d.values.total || 0) / 100, { + trailingZeroDisplay: "stripIfInteger", + })}- {currencyFormatter(valueAccessor(d))} + {currencyFormatter(valueAccessor(d), { + trailingZeroDisplay: "stripIfInteger", + })}Also applies to: 204-205
packages/email/src/templates/new-sale-alert-partner.tsx (1)
58-60: Clarify preview copy: it shows commission, not sale amount.Preview reads “You just made a {earningsInDollars} sale…”, but
earningsInDollarsis commission. Consider rewording to avoid confusion.- You just made a {earningsInDollars} sale via your referral link{" "} + You just earned {earningsInDollars} via your referral link{" "}apps/web/ui/partners/bounties/bounty-performance.tsx (1)
32-39: Guard zero/invalid targets and show 0% progress.If
targetis 0 or negative,value/targetyields Infinity/NaN and the bar can incorrectly render as full width. Also,value && (...)hides a valid 0% bar.- <div className="h-1.5 w-full overflow-hidden rounded-full bg-neutral-200"> - {value && ( + <div className="h-1.5 w-full overflow-hidden rounded-full bg-neutral-200"> + {typeof value === "number" && ( <div style={{ - width: Math.min(Math.max(value / target, 0), 1) * 100 + "%", + width: + (target > 0 + ? Math.min(Math.max(value / target, 0), 1) + : 0) * + 100 + "%", }} className="h-full rounded-full bg-orange-600" /> )} </div>apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)
86-86: Prefer a cents-to-currency helper to avoid repeated/100.This math appears many times across the app. A tiny wrapper improves consistency and reduces chance of accidental double division.
Apply locally:
- Total: currencyFormatter(payout.amount / 100), + Total: currencyFromCents(payout.amount),Helper (outside this file):
// packages/utils/src/functions/currency-formatter.ts export const currencyFromCents = ( cents: number, options?: CurrencyFormatterOptions, ) => currencyFormatter(cents / 100, options);apps/web/ui/partners/mark-commission-duplicate-modal.tsx (1)
82-86: Inline/100reads well, but a shared helper is safer.Minor DX/consistency win: reuse a
currencyFromCents(...)wrapper.- ? currencyFormatter(commission.amount / 100) + ? currencyFromCents(commission.amount) @@ - Commission: currencyFormatter(commission.earnings / 100), + Commission: currencyFromCents(commission.earnings),(See suggested helper in previous comment.)
apps/web/ui/links/link-builder/link-partner-details.tsx (1)
57-61: Nice simplification; consider a sharedcurrencyFromCentshelper.Removes the bespoke helper and standardizes on the utils formatter. Small follow-up: use a cents wrapper to eliminate inline division and ensure consistency everywhere.
- partner ? currencyFormatter(partner.saleAmount / 100) : undefined, + partner ? currencyFromCents(partner.saleAmount) : undefined, @@ - ? currencyFormatter(partner.totalCommissions / 100) + ? currencyFromCents(partner.totalCommissions) @@ - partner ? currencyFormatter(partner.netRevenue / 100) : undefined, + partner ? currencyFromCents(partner.netRevenue) : undefined,Helper shown in the first file’s comment.
Also applies to: 64-66, 69-70
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx (1)
74-86: Guard percentage calc against divide-by-zeroIf invoice.amount can be 0, Math.round((invoice.fee / invoice.amount) * 100) will throw. Add a safe fallback.
- { - label: `Platform fees (${Math.round((invoice.fee / invoice.amount) * 100)}%)`, - value: `${currencyFormatter(invoice.fee / 100)}`, - }, + { + label: `Platform fees (${ + invoice.amount ? Math.round((invoice.fee / invoice.amount) * 100) : 0 + }%)`, + value: `${currencyFormatter(invoice.fee / 100)}`, + },apps/web/ui/partners/partner-details-sheet.tsx (2)
151-157: stripIfInteger + minFractionDigits=2 still shows 1.50; set min to 0 for true “dynamic” decimalsWith the utils default min/max = 2, trailingZeroDisplay: "stripIfInteger" only strips integer ".00". If you want 1.5 instead of 1.50, override minimumFractionDigits to 0.
- : currencyFormatter(partner.saleAmount / 100, { - trailingZeroDisplay: "stripIfInteger", - }), + : currencyFormatter(partner.saleAmount / 100, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + trailingZeroDisplay: "stripIfInteger", + }),
388-403: Match “dynamic decimals” intent for link revenueSame as above: override minimumFractionDigits for non-integers to avoid 1.50.
- accessorFn: (d) => - currencyFormatter(d.saleAmount / 100, { - trailingZeroDisplay: "stripIfInteger", - }), + accessorFn: (d) => + currencyFormatter(d.saleAmount / 100, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + trailingZeroDisplay: "stripIfInteger", + }), ... - {currencyFormatter(row.original.saleAmount / 100, { - trailingZeroDisplay: "stripIfInteger", - })} + {currencyFormatter(row.original.saleAmount / 100, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + trailingZeroDisplay: "stripIfInteger", + })}apps/web/lib/integrations/slack/transform.ts (1)
154-190: Optional: unify sale amount formatting and handle currency exponentscreateSaleTemplate still hand-rolls toFixed(2) and appends the code. Consider using currencyFormatter with proper exponent handling for non-USD currencies, consistent with the invoice fix.
- const amountInDollars = (sale.amount / 100).toFixed(2); + const digits = new Intl.NumberFormat("en-US", { + style: "currency", + currency: sale.currency.toUpperCase(), + }).resolvedOptions().maximumFractionDigits; + const amountInMajor = sale.amount / Math.pow(10, digits); ... - text: `*Amount*\n${amountInDollars} ${sale.currency.toUpperCase()}`, + text: `*Amount*\n${currencyFormatter(amountInMajor, { + currency: sale.currency.toUpperCase(), + })}`,packages/utils/src/functions/currency-formatter.ts (2)
11-16: Non-USD overrides will still force 2 decimals. Prefer currency-native minor units whenoptions.currencyis provided.Today JPY/EUR overrides would render with 2 decimals unless callers remember to set digits. Make the default smarter.
- Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 2, - maximumFractionDigits: 2, - trailingZeroDisplay: "auto", - ...options, - } as CurrencyFormatterOptions).format(value); + { + const defaults: CurrencyFormatterOptions = { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + trailingZeroDisplay: "auto", + }; + const merged: CurrencyFormatterOptions = { ...defaults, ...options }; + // If caller overrides currency and didn't explicitly set fraction digits, + // use that currency's native minor units (defer to Intl defaults). + if (options?.currency && + options.minimumFractionDigits === undefined && + options.maximumFractionDigits === undefined) { + delete (merged as any).minimumFractionDigits; + delete (merged as any).maximumFractionDigits; + } + return new Intl.NumberFormat("en-US", merged).format(value); + }
11-16: Micro-perf: cache the default formatter.Most calls appear to use defaults; reuse a single
Intl.NumberFormatinstance.+const defaultCurrencyFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + trailingZeroDisplay: "auto", +} as CurrencyFormatterOptions); + export const currencyFormatter = ( value: number, options?: CurrencyFormatterOptions, ) => - Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 2, - maximumFractionDigits: 2, - trailingZeroDisplay: "auto", - ...options, - } as CurrencyFormatterOptions).format(value); + (options + ? new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + trailingZeroDisplay: "auto", + ...options, + } as CurrencyFormatterOptions) + : defaultCurrencyFormatter + ).format(value);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (64)
apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/client.tsx(1 hunks)apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/client.tsx(1 hunks)apps/web/app/(ee)/admin.dub.co/(dashboard)/revenue/client.tsx(1 hunks)apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts(2 hunks)apps/web/app/(ee)/app.dub.co/embed/referrals/earnings-summary.tsx(1 hunks)apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx(1 hunks)apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx(1 hunks)apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx(3 hunks)apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx(3 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx(2 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-composite-chart.tsx(2 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx(2 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/invoices/[payoutId]/route.tsx(1 hunks)apps/web/app/api/callback/plain/partner/route.ts(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-timeseries-chart.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/overview-chart.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx(1 hunks)apps/web/lib/api/sales/construct-reward-amount.ts(2 hunks)apps/web/lib/integrations/slack/transform.ts(3 hunks)apps/web/ui/analytics/events/events-table.tsx(1 hunks)apps/web/ui/customers/customer-details-column.tsx(1 hunks)apps/web/ui/customers/customer-partner-earnings-table.tsx(1 hunks)apps/web/ui/customers/customer-sales-table.tsx(2 hunks)apps/web/ui/customers/customer-table/customer-table.tsx(1 hunks)apps/web/ui/layout/sidebar/payout-stats.tsx(0 hunks)apps/web/ui/links/link-analytics-badge.tsx(1 hunks)apps/web/ui/links/link-builder/link-partner-details.tsx(1 hunks)apps/web/ui/partners/bounties/bounty-performance.tsx(1 hunks)apps/web/ui/partners/mark-commission-duplicate-modal.tsx(1 hunks)apps/web/ui/partners/mark-commission-fraud-or-canceled-modal.tsx(1 hunks)apps/web/ui/partners/overview/blocks/commissions-block.tsx(1 hunks)apps/web/ui/partners/overview/blocks/countries-block.tsx(1 hunks)apps/web/ui/partners/overview/blocks/links-block.tsx(1 hunks)apps/web/ui/partners/overview/blocks/partners-block.tsx(1 hunks)apps/web/ui/partners/overview/blocks/traffic-sources-block.tsx(1 hunks)apps/web/ui/partners/partner-details-sheet.tsx(4 hunks)apps/web/ui/partners/payout-invoice-sheet.tsx(4 hunks)apps/web/ui/partners/program-reward-modifiers-tooltip.tsx(1 hunks)apps/web/ui/partners/rewards/rewards-logic.tsx(1 hunks)packages/email/src/templates/connect-payout-reminder.tsx(1 hunks)packages/email/src/templates/new-sale-alert-partner.tsx(1 hunks)packages/email/src/templates/new-sale-alert-program-owner.tsx(1 hunks)packages/email/src/templates/partner-payout-confirmed.tsx(1 hunks)packages/email/src/templates/partner-payout-failed.tsx(1 hunks)packages/email/src/templates/partner-payout-processed.tsx(1 hunks)packages/email/src/templates/partner-payout-withdrawal-completed.tsx(0 hunks)packages/email/src/templates/partner-payout-withdrawal-initiated.tsx(0 hunks)packages/email/src/templates/partner-paypal-payout-failed.tsx(1 hunks)packages/email/src/templates/partner-program-summary.tsx(2 hunks)packages/email/src/templates/program-payout-reminder.tsx(1 hunks)packages/utils/src/functions/currency-formatter.ts(1 hunks)
💤 Files with no reviewable changes (3)
- apps/web/ui/layout/sidebar/payout-stats.tsx
- packages/email/src/templates/partner-payout-withdrawal-completed.tsx
- packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/partner-program-summary.tsxapps/web/app/api/callback/plain/partner/route.tspackages/email/src/templates/new-sale-alert-program-owner.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
📚 Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts
🧬 Code graph analysis (56)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/overview-chart.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/overview/blocks/partners-block.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/partner-program-summary.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/payout-invoice-sheet.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/program-payout-reminder.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-composite-chart.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/invoices/[payoutId]/route.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/overview/blocks/commissions-block.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/overview/blocks/traffic-sources-block.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/lib/api/sales/construct-reward-amount.ts (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/overview/blocks/countries-block.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/bounties/bounty-performance.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/partner-payout-confirmed.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/partner-paypal-payout-failed.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/partner-payout-failed.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/admin.dub.co/(dashboard)/payouts/client.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/app.dub.co/embed/referrals/earnings-summary.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/client.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/customers/customer-sales-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/partner-payout-processed.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/mark-commission-duplicate-modal.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/new-sale-alert-partner.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/rewards/rewards-logic.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/links/link-builder/link-partner-details.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-timeseries-chart.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/api/callback/plain/partner/route.ts (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/customers/customer-partner-earnings-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/overview/blocks/links-block.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/connect-payout-reminder.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
packages/email/src/templates/new-sale-alert-program-owner.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/partner-details-sheet.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/admin.dub.co/(dashboard)/revenue/client.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/ui/partners/mark-commission-fraud-or-canceled-modal.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/lib/integrations/slack/transform.ts (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
🪛 Biome (2.1.2)
apps/web/lib/integrations/slack/transform.ts
[error] 285-285: Unexpected empty object pattern.
(lint/correctness/noEmptyPattern)
[error] 294-294: Unexpected empty object pattern.
(lint/correctness/noEmptyPattern)
⏰ 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: Vade Review
🔇 Additional comments (48)
apps/web/app/(ee)/partners.dub.co/invoices/[payoutId]/route.tsx (1)
118-118: LGTM: default formatter keeps 2 decimals (invoice-safe).Dropping the explicit fraction options retains the same output given the utility’s defaults. No behavior change here.
apps/web/ui/customers/customer-table/customer-table.tsx (1)
164-165: LGTM: clearer currency display.Using trailingZeroDisplay: "stripIfInteger" improves readability while preserving cents when present.
apps/web/ui/analytics/events/events-table.tsx (1)
213-215: LGTM: consistent currency formatting for amounts.Matches the Customers table pattern; good consistency.
apps/web/ui/customers/customer-details-column.tsx (1)
163-164: Good use of trailingZeroDisplayNice touch to drop “.00” for integers. Ensure all target runtimes (Node/Edge/browsers) support Intl trailingZeroDisplay in your deployment targets.
packages/email/src/templates/partner-payout-confirmed.tsx (1)
45-45: Check remaining currencyFormatter overrides
Explicit overrides detected in:
- packages/email/src/templates/partner-payout-withdrawal-completed.tsx (line 35)
- packages/email/src/templates/partner-payout-withdrawal-initiated.tsx (line 33)
Confirm these align with the new default (USD, two decimals) or remove the custom options.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx (1)
233-234: Consistent currency formatting with new defaults.Good swap to currencyFormatter(value / 100); matches the standardized formatter.
Double-check that performanceCondition.value (target) cannot be 0; ProgressCircle uses value/target and would divide by zero. If 0 is possible, guard or clamp before computing progress.
apps/web/app/(ee)/app.dub.co/embed/referrals/earnings-summary.tsx (1)
47-47: Embed: formatting aligned with utility defaults.Looks correct; keeps USD with 2 decimals by default.
packages/email/src/templates/connect-payout-reminder.tsx (1)
99-99: Email template: standardized currency display.Change is consistent with the utility’s default USD/2-decimal behavior.
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1)
67-67: Partner payouts: formatter refactor looks good.Keeps prior display semantics via defaults.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx (1)
116-116: LGTM — aligned with centralized currency defaultsReplacing per-call options with the standardized formatter is correct here.
apps/web/ui/partners/overview/blocks/links-block.tsx (1)
88-88: LGTM — uses shared formatter defaultsConsistent with the refactor; no issues.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)
312-312: LGTM — unified currency formattingMatches the shared defaults; tooltip copies already reuse the same helper.
apps/web/app/api/callback/plain/partner/route.ts (1)
222-223: LGTM — consistent formatter in Plain card usageStandardizes display; expected two-decimal output in USD.
apps/web/ui/customers/customer-sales-table.tsx (1)
54-55: LGTM – consistent formatter usage.Switch to the shared currencyFormatter with defaults reads well and keeps logic centralized.
Also applies to: 63-64
apps/web/ui/customers/customer-partner-earnings-table.tsx (1)
37-38: LGTM – consistent formatter usage.The cells now rely on the standardized currencyFormatter; no issues spotted.
Also applies to: 44-45
apps/web/ui/partners/overview/blocks/traffic-sources-block.tsx (1)
91-92: LGTM – simplified currency formatting.Matches the project-wide refactor; no functional changes beyond centralizing defaults.
packages/email/src/templates/partner-payout-processed.tsx (1)
44-44: LGTM — consistent formatter usage in emails.Using the shared formatter keeps email totals aligned with UI.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1)
238-238: LGTM — formatter unification in table cells.Single-source currency formatting simplifies consistency across tooltips and cells.
apps/web/ui/partners/mark-commission-fraud-or-canceled-modal.tsx (1)
89-92: Currency formatting refactor: LGTMSwapping to the defaulted
currencyFormatter(x / 100)keeps behavior consistent with the new util. No issues spotted.apps/web/ui/partners/overview/blocks/commissions-block.tsx (1)
70-70: Adopts standardized formatter correctlyUsing
currencyFormatter(earnings / 100)aligns with the new defaults. Looks good.apps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsx (1)
83-83: Formatter usage updated correctlySingle-argument
currencyFormatter(totalCommissions / 100)matches the new API. Good to go.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx (1)
125-125: Groups monetary columns correctly migratedRevenue, Commissions, and Net Revenue now use the standardized formatter. All good.
Also applies to: 130-130, 135-135
apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (1)
145-145: LGTM — correct cents-to-dollars formatting.Using currencyFormatter(Number(condition.value) / 100) matches the stored cents semantics for currency conditions.
packages/email/src/templates/partner-program-summary.tsx (1)
159-161: LGTM — standardized currency formatting (cents ➜ dollars).Both monthly and lifetime earnings now consistently use currencyFormatter(… / 100).
Also applies to: 179-181
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/overview-chart.tsx (1)
159-159: Tooltip currency formatting now relies on default USD/2-decimal settings — looks good.Matches the updated currencyFormatter defaults and stays consistent with the YAxis tickFormat. No behavior change expected.
packages/email/src/templates/new-sale-alert-partner.tsx (1)
50-53: Simplified currency formatting — consistent with new defaults.Switching to
currencyFormatter(x / 100)preserves two decimals under the new utility defaults. Safe change.apps/web/ui/partners/bounties/bounty-performance.tsx (1)
19-19: Currency formatter refactor aligns with new defaults.Using the default formatter for currency attributes is correct and consistent with the PR’s direction.
packages/email/src/templates/program-payout-reminder.tsx (1)
77-77: Currency formatter simplification — OK.Two-decimal USD output is preserved via the new defaults.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-timeseries-chart.tsx (1)
57-58: Tooltip currency formatting uses new defaults — consistent with YAxis.Keeps tooltip and axis labels aligned on USD with two decimals.
packages/email/src/templates/new-sale-alert-program-owner.tsx (1)
69-75: LGTM on switching to the formatter defaults.Division by 100 and relying on the new defaults is consistent with the updated utility.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (2)
86-86: LGTM: rely on default currency formatting.
165-166: LGTM: consistent table currency formatting.apps/web/app/(ee)/admin.dub.co/(dashboard)/revenue/client.tsx (1)
92-93: LGTM: simplified currency formatting call.apps/web/lib/api/sales/construct-reward-amount.ts (1)
42-45: LGTM: consistent use of shared formatter; clear handling of percentage vs. currency.Also applies to: 56-57
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)
165-165: LGTM — uses standardized formatter defaults.Consistent with the refactor goals.
apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx (1)
59-60: LGTM — aligns embed with unified currency formatting.No behavioral change vs prior explicit 2-decimal calls; just cleaner.
Also applies to: 67-68
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx (2)
154-155: LGTM — formatter usage matches project defaults.Keeps negative values readable; color is already handled elsewhere.
141-147: Use a numeric accessor for sorting and format incell
AccessorFn currently returns formatted strings (currencyFormatter/nFormatter), which most table libs sort lexicographically (e.g."$90">"$1,200"). ChangeaccessorFnto return the raw number (d.amountord.quantity) and move formatting into acellrenderer—or use a dedicatedsortValue/sortingFnif supported.apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx (1)
167-183: LGTM on formatter simplificationUsing the shared currencyFormatter without explicit fraction options keeps this consistent with the PR’s direction.
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx (2)
204-208: LGTM: total formatting via shared formatterConsistent with the new utility.
233-236: LGTM: per-domain renewal fee formattingReads clearly and aligns with the new formatter.
apps/web/ui/partners/partner-details-sheet.tsx (1)
269-272: LGTM: payouts amount columnGood move to the shared formatter; keeps the table consistent.
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx (3)
137-149: LGTM: summary values use shared formatterFormatting reads clean; pairs well with the non-USD note above once fixed.
279-281: LGTM: header total formattingConsistent with other invoice totals.
339-340: LGTM: per-payout amount formattingLooks good.
apps/web/lib/integrations/slack/transform.ts (1)
250-253: LGTM: switch to shared currencyFormatterSimplifies and unifies Slack amounts.
packages/utils/src/functions/currency-formatter.ts (2)
12-14: Confirm the product shift to always show cents by default.Switching from
stripIfIntegertoautomeans 10 →$10.00(not$10). Verify Slack/email templates and UI copy expect this.
7-8: Signature change is non-breaking; no callers pass a currency string
Ran a regex across all call sites—no instances ofcurrencyFormatter(..., 'USD')or similar string literals as the second argument; existing two-arg usages all pass an options object.
| if (availableBalance <= 0) { | ||
| return logAndRespond( | ||
| `The available balance (${currencyFormatter(availableBalance / 100, { maximumFractionDigits: 2, currency })}) for partner ${partner.email} (${stripeAccount}) is less than or equal to 0 after subtracting pending payouts. Skipping...`, | ||
| `The available balance (${currencyFormatter(availableBalance / 100, { currency })}) for partner ${partner.email} (${stripeAccount}) is less than or equal to 0 after subtracting pending payouts. Skipping...`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: dividing by 100 is incorrect for non-2-decimal currencies
Stripe amounts are in minor units. Using /100 breaks for zero-decimal (e.g., JPY, HUF, TWD) and 3-decimal (e.g., KWD, BHD) currencies. Convert by currency exponent and normalize the code to uppercase for Intl.
Apply this minimal change:
- `The available balance (${currencyFormatter(availableBalance / 100, { currency })}) for partner ${partner.email} (${stripeAccount}) is less than or equal to 0 after subtracting pending payouts. Skipping...`,
+ `The available balance (${currencyFormatter(minorToMajor(availableBalance, currency), { currency: currency.toUpperCase() })}) for partner ${partner.email} (${stripeAccount}) is less than or equal to 0 after subtracting pending payouts. Skipping...`,Helper (outside this hunk):
const EXPONENT: Record<string, number> = {
// zero-decimal
jpy: 0, huf: 0, twd: 0, krw: 0, vnd: 0, clp: 0, pyg: 0, rwf: 0, bif: 0,
djf: 0, gnf: 0, kmf: 0, mga: 0, vuv: 0, xaf: 0, xof: 0, xpf: 0,
// three-decimal
kwd: 3, bhd: 3, jod: 3, omr: 3, tnd: 3, iqd: 3,
};
const minorToMajor = (amountMinor: number, ccy: string) =>
amountMinor / 10 ** (EXPONENT[ccy.toLowerCase()] ?? 2);🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts around line 83,
the template currently divides availableBalance by 100 which is wrong for
currencies with non-2 decimal exponents; add the provided EXPONENT map and
minorToMajor helper outside this hunk, then replace the /100 conversion with
minorToMajor(availableBalance, currency) and ensure the currency passed to Intl
formatting is normalized to uppercase (e.g., { currency: currency.toUpperCase()
}) so amounts are correctly converted and formatted for zero-, two- and
three-decimal currencies.
|
|
||
| console.log( | ||
| `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(stripePayout.amount / 100, { maximumFractionDigits: 2, currency: stripePayout.currency })})`, | ||
| `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(stripePayout.amount / 100, { currency: stripePayout.currency })})`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Same currency-scale issue here + normalize currency
Use exponent-aware conversion and uppercase the currency code for Intl.
- `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(stripePayout.amount / 100, { currency: stripePayout.currency })})`,
+ `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(minorToMajor(stripePayout.amount, stripePayout.currency), { currency: stripePayout.currency.toUpperCase() })})`,📝 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.
| `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(stripePayout.amount / 100, { currency: stripePayout.currency })})`, | |
| `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(minorToMajor(stripePayout.amount, stripePayout.currency), { currency: stripePayout.currency.toUpperCase() })})`, |
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts around line
107, the payout log uses a naive cents-to-major conversion and a lowercase
currency code; change it to use an exponent-aware conversion (divide
stripePayout.amount by 10**minorUnits for the currency) and normalize the
currency code to uppercase before formatting with Intl.NumberFormat so the
displayed amount and currency are correct for currencies with different
minor-unit scales.
| const nonUsdTransactionDisplay = | ||
| chargeAmount && chargeCurrency && chargeCurrency !== "usd" | ||
| ? ` (${currencyFormatter( | ||
| chargeAmount / 100, | ||
| { | ||
| minimumFractionDigits: 2, | ||
| maximumFractionDigits: 2, | ||
| }, | ||
| chargeCurrency.toUpperCase(), | ||
| )})` | ||
| ? ` (${currencyFormatter(chargeAmount / 100, { | ||
| currency: chargeCurrency.toUpperCase(), | ||
| })})` | ||
| : ""; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect minor-unit conversion for non-USD currencies
Stripe amounts are in the currency’s smallest unit (0/2/3 digits). Dividing by 100 will be wrong for JPY (0) and KWD/BHD/JOD… (3). Convert using the currency’s fraction digits.
- const nonUsdTransactionDisplay =
- chargeAmount && chargeCurrency && chargeCurrency !== "usd"
- ? ` (${currencyFormatter(chargeAmount / 100, {
- currency: chargeCurrency.toUpperCase(),
- })})`
- : "";
+ const nonUsdTransactionDisplay =
+ chargeAmount && chargeCurrency && chargeCurrency !== "usd"
+ ? ` (${currencyFormatter(
+ chargeAmount /
+ Math.pow(
+ 10,
+ new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: chargeCurrency.toUpperCase(),
+ }).resolvedOptions().maximumFractionDigits,
+ ),
+ { currency: chargeCurrency.toUpperCase() },
+ )})`
+ : "";📝 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 nonUsdTransactionDisplay = | |
| chargeAmount && chargeCurrency && chargeCurrency !== "usd" | |
| ? ` (${currencyFormatter( | |
| chargeAmount / 100, | |
| { | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 2, | |
| }, | |
| chargeCurrency.toUpperCase(), | |
| )})` | |
| ? ` (${currencyFormatter(chargeAmount / 100, { | |
| currency: chargeCurrency.toUpperCase(), | |
| })})` | |
| : ""; | |
| const nonUsdTransactionDisplay = | |
| chargeAmount && chargeCurrency && chargeCurrency !== "usd" | |
| ? ` (${currencyFormatter( | |
| chargeAmount / | |
| Math.pow( | |
| 10, | |
| new Intl.NumberFormat("en-US", { | |
| style: "currency", | |
| currency: chargeCurrency.toUpperCase(), | |
| }).resolvedOptions().maximumFractionDigits, | |
| ), | |
| { currency: chargeCurrency.toUpperCase() }, | |
| )})` | |
| : ""; |
🤖 Prompt for AI Agents
In apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx
around lines 130-136, the code divides Stripe amounts by 100 for non-USD
currencies which is incorrect for currencies with 0 or 3 minor digits; instead,
determine the currency’s fraction digits (e.g. via new
Intl.NumberFormat(undefined, { style: 'currency', currency: chargeCurrency
}).resolvedOptions().maximumFractionDigits) and compute divisor = 10 **
fractionDigits, then convert the integer amount by dividing by that divisor
before passing to currencyFormatter; ensure chargeCurrency is upper-cased and
handle missing values safely.
| accessorFn: (d) => currencyFormatter(d.saleAmount / 100), | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Ensure numeric sorting for Revenue (use accessorKey + cell formatting).
Returning a formatted string from accessorFn causes lexicographic sorting on "saleAmount". Use the numeric field as the accessor and format in cell.
- accessorFn: (d) => currencyFormatter(d.saleAmount / 100),
+ accessorKey: "saleAmount",
+ cell: ({ getValue }) => currencyFormatter(Number(getValue()) / 100),
+ sortingFn: "basic",Tip: Apply the same pattern to other numeric columns that currently return formatted strings to avoid string-based sorts.
📝 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.
| accessorFn: (d) => currencyFormatter(d.saleAmount / 100), | |
| }, | |
| accessorKey: "saleAmount", | |
| cell: ({ getValue }) => currencyFormatter(Number(getValue()) / 100), | |
| sortingFn: "basic", |
🤖 Prompt for AI Agents
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
around lines 104-105: the column currently returns a formatted string from
accessorFn (currencyFormatter(d.saleAmount / 100)) which causes lexicographic
sorting; change the column to use a numeric accessorKey (saleAmount) so the
table sorts by the raw number and move the formatting into the column's cell
renderer (format the cell value for display by dividing by 100 and applying
currencyFormatter); apply the same accessorKey + cell formatting pattern to any
other numeric columns that currently return formatted strings to ensure correct
numeric sorting.
| {currencyFormatter(amount / 100, {})} | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Drop “USD” suffix and avoid passing empty options.
currencyFormatter already outputs USD; appending " USD" duplicates the currency. Also, {} is unnecessary noise.
Apply:
- {currencyFormatter(amount / 100, {})}
+ {currencyFormatter(amount / 100)}- {currencyFormatter(
- eligiblePendingPayouts?.amount
- ? eligiblePendingPayouts.amount / 100
- : 0,
- {},
- ) + " USD"}
+ {currencyFormatter(
+ eligiblePendingPayouts?.amount
+ ? eligiblePendingPayouts.amount / 100
+ : 0
+ )}- {currencyFormatter(amount / 100, {})}
+ {currencyFormatter(amount / 100)}- {currencyFormatter(totalPaid / 100, {}) + " USD"}
+ {currencyFormatter(totalPaid / 100)}Also applies to: 123-129, 176-177, 185-186
🤖 Prompt for AI Agents
In
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx
around lines 114-116 (also update the calls at 123-129, 176-177, and 185-186),
remove the explicit " USD" suffix being appended to the output of
currencyFormatter and stop passing an empty options object ({}); instead call
currencyFormatter(amount / 100) and render that result directly so the currency
is not duplicated and the unnecessary empty argument is eliminated.
💡 Verification agent
🧩 Analysis chain
Repo-wide sweep for “ USD” after formatted values.
There may be other instances. Run this to surface duplicates and clean them up.
Also applies to: 123-129, 176-177, 185-186
🏁 Script executed:
#!/bin/bash
# Find places where currencyFormatter output is followed by a manual " USD"
rg -nP --type=ts --type=tsx -C2 'currencyFormatter\([^)]*\)\s*\+\s*[\'"] USD[\'"]'
# Find JSX like {...currencyFormatter(...)} USD
rg -nP --type=ts --type=tsx -C2 '\}\s*USD\b'Length of output: 116
🏁 Script executed:
#!/bin/bash
set -eo pipefail
# Find concatenated currencyFormatter output with manual " USD"
rg -nP --type ts --type tsx -C2 "currencyFormatter\([^)]*\)\s*\+\s*['\"][ ]*USD['\"]"
# Find JSX patterns like {currencyFormatter(...)} USD
rg -nP --type ts --type tsx -C2 "\{\s*currencyFormatter\([^)]*\)\s*\}\s*USD\b"
# Find any standalone "} USD" cases
rg -nP --type ts --type tsx -C2 "\}\s*USD\b"Length of output: 138
#!/bin/bash
set -eo pipefail
# Search all .ts and .tsx files for currencyFormatter concatenated with " USD"
rg -nP -g '*.ts' -g '*.tsx' -C2 "currencyFormatter\\([^)]*\\)\\s*\\+\\s*['\"]\\s*USD['\"]"
# Search for JSX patterns where formatted value is directly followed by USD
rg -nP -g '*.ts' -g '*.tsx' -C2 "\\{\\s*currencyFormatter\\([^)]*\\)\\s*\\}\\s*USD\\b"
# Catch any standalone "} USD" sequences
rg -nP -g '*.ts' -g '*.tsx' -C2 "\\}\\s*USD\\b"#!/bin/bash
set -eo pipefail
# Find lines where currencyFormatter and USD appear on the same line
rg -n 'currencyFormatter.*USD' -g '*.{ts,tsx}' -C2
# Find files that use currencyFormatter and then manual " USD" in separate lines
files=$(rg -l 'currencyFormatter' -g '*.{ts,tsx}')
if [ -n "$files" ]; then
rg -n -C2 'USD' $files
fi🏁 Script executed:
#!/bin/bash
set -eo pipefail
# Find concatenated currencyFormatter output with manual " USD"
rg -nP -g '*.ts' -g '*.tsx' -C2 "currencyFormatter\\([^)]*\\)\\s*\\+\\s*['\"]\\s*USD['\"]"
# Find JSX patterns like {currencyFormatter(...)} USD
rg -nP -g '*.ts' -g '*.tsx' -C2 "\\{\\s*currencyFormatter\\([^)]*\\)\\s*\\}\\s*USD\\b"
# Catch any standalone "} USD" or " USD" following formatted expressions
rg -nP -g '*.ts' -g '*.tsx' -C2 "(currencyFormatter|\\})\\s*USD\\b"Length of output: 830
🏁 Script executed:
#!/bin/bash
set -eo pipefail
file="apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx"
sed -n '110,130p' "$file"
sed -n '170,190p' "$file"Length of output: 1956
Remove manual “ USD” concatenations
In apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx, remove the + " USD" on the two Tooltip spans around eligiblePendingPayouts and totalPaid and adjust currencyFormatter to include the currency code. No other instances were found.
🤖 Prompt for AI Agents
In
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx
around lines 114-116, remove the manual ' + " USD"' concatenations on the two
Tooltip span outputs for eligiblePendingPayouts and totalPaid and instead call
currencyFormatter with an option or parameter to include the currency code
(e.g., pass { currency: 'USD', display: 'code' } or equivalent supported
option), and update the currencyFormatter call sites accordingly so the
formatted string already contains "USD" without manual concatenation.
| "Commissions", | ||
| !partner.totalCommissions | ||
| ? "-" | ||
| : currencyFormatter(partner.totalCommissions / 100, { | ||
| minimumFractionDigits: 2, | ||
| maximumFractionDigits: 2, | ||
| }), | ||
| : currencyFormatter(partner.totalCommissions / 100), | ||
| ], | ||
| [ | ||
| "Net revenue", | ||
| !partner.netRevenue | ||
| ? "-" | ||
| : currencyFormatter(partner.netRevenue / 100, { | ||
| minimumFractionDigits: 2, | ||
| maximumFractionDigits: 2, | ||
| }), | ||
| : currencyFormatter(partner.netRevenue / 100), | ||
| ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t hide zero monetary values
Using !value will render "-" for 0. Prefer a nullish check so $0.00 displays correctly.
- [
- "Commissions",
- !partner.totalCommissions
- ? "-"
- : currencyFormatter(partner.totalCommissions / 100),
- ],
+ [
+ "Commissions",
+ partner.totalCommissions == null
+ ? "-"
+ : currencyFormatter(partner.totalCommissions / 100),
+ ],
- [
- "Net revenue",
- !partner.netRevenue
- ? "-"
- : currencyFormatter(partner.netRevenue / 100),
- ],
+ [
+ "Net revenue",
+ partner.netRevenue == null
+ ? "-"
+ : currencyFormatter(partner.netRevenue / 100),
+ ],📝 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.
| "Commissions", | |
| !partner.totalCommissions | |
| ? "-" | |
| : currencyFormatter(partner.totalCommissions / 100, { | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 2, | |
| }), | |
| : currencyFormatter(partner.totalCommissions / 100), | |
| ], | |
| [ | |
| "Net revenue", | |
| !partner.netRevenue | |
| ? "-" | |
| : currencyFormatter(partner.netRevenue / 100, { | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 2, | |
| }), | |
| : currencyFormatter(partner.netRevenue / 100), | |
| ], | |
| [ | |
| "Commissions", | |
| partner.totalCommissions == null | |
| ? "-" | |
| : currencyFormatter(partner.totalCommissions / 100), | |
| ], | |
| [ | |
| "Net revenue", | |
| partner.netRevenue == null | |
| ? "-" | |
| : currencyFormatter(partner.netRevenue / 100), | |
| ], |
🤖 Prompt for AI Agents
In apps/web/ui/partners/partner-details-sheet.tsx around lines 159–169, the code
uses !partner.totalCommissions and !partner.netRevenue which treats 0 as falsy
and renders "-" incorrectly; change those checks to nullish checks (e.g.,
partner.totalCommissions == null ? "-" :
currencyFormatter(partner.totalCommissions / 100)) and similarly for
partner.netRevenue so actual $0.00 is displayed while null/undefined still shows
"-".
| currencyFormatter(fee / 100) | ||
| ) : ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Round to integer cents before formatting and submitting.
fee and total are computed as floating-point cents (percentage of amount). To avoid fractional-cent drift between display and backend, round to integer cents when formatting and when sending to confirmPayouts.
Apply rounding in the formatted displays:
- currencyFormatter(fee / 100)
+ currencyFormatter(Math.round(fee) / 100)- currencyFormatter(total / 100)
+ currencyFormatter(Math.round(total) / 100)- ? `Hold to confirm ${currencyFormatter(amount / 100)} payout`
+ ? `Hold to confirm ${currencyFormatter(Math.round(amount) / 100)} payout`Also update the action payload to send integer cents (outside the changed lines; shown here for completeness):
// inside onClick
const amountCents = Math.round(amount ?? 0);
const feeCents = Math.round(fee ?? 0);
const totalCents = Math.round(total ?? 0);
const result = await confirmPayouts({
workspaceId,
paymentMethodId: selectedPaymentMethod.id,
cutoffPeriod,
excludedPayoutIds,
amount: amountCents,
fee: feeCents,
total: totalCents,
});Also applies to: 299-301, 483-485
🤖 Prompt for AI Agents
In apps/web/ui/partners/payout-invoice-sheet.tsx around lines 273-274 (also
apply same change at 299-301 and 483-485), the fee/total are floating-point
cents and must be rounded to integer cents before formatting and before sending
to confirmPayouts; update the formatted displays to call Math.round on fee/total
(and amount where shown) before passing to currencyFormatter, and update the
onClick action payload (outside these lines) to compute const amountCents =
Math.round(amount ?? 0); const feeCents = Math.round(fee ?? 0); const totalCents
= Math.round(total ?? 0); and send those integer cents in the confirmPayouts
call.
| const saleAmountInDollars = currencyFormatter(sale.amount / 100); | ||
|
|
||
| const earningsInDollars = currencyFormatter(sale.earnings / 100, { | ||
| minimumFractionDigits: 2, | ||
| maximumFractionDigits: 2, | ||
| }); | ||
| const earningsInDollars = currencyFormatter(sale.earnings / 100); | ||
|
|
||
| const profitInDollars = currencyFormatter( | ||
| (sale.amount - sale.earnings) / 100, | ||
| { | ||
| minimumFractionDigits: 2, | ||
| maximumFractionDigits: 2, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicate “USD” suffix and format negatives via formatter.
currencyFormatter already returns a USD-formatted string (e.g., “$13.30”). Appending “ USD” renders “$13.30 USD”. Also, prefer passing a negative value to the formatter rather than prepending “-”.
Apply:
- const earningsInDollars = currencyFormatter(sale.earnings / 100);
+ const earningsInDollars = currencyFormatter(-sale.earnings / 100);- {saleAmountInDollars} USD
+ {saleAmountInDollars}- -{earningsInDollars} USD
+ {earningsInDollars}- {profitInDollars} USD
+ {profitInDollars}Also applies to: 134-136, 146-148, 160-163
🤖 Prompt for AI Agents
In packages/email/src/templates/new-sale-alert-program-owner.tsx around lines
69-75 (and likewise at 134-136, 146-148, 160-163), the code appends a literal "
USD" to values returned by currencyFormatter and prefixes negatives manually;
remove the extra " USD" concatenation and instead pass the numeric value
(positive or negative) into currencyFormatter so it produces properly formatted
negative values (e.g., use currencyFormatter((sale.amount - sale.earnings) /
100) for profit, and for any negative display pass a negative numeric value
rather than adding a '-' string).
| interface CurrencyFormatterOptions extends Intl.NumberFormatOptions { | ||
| trailingZeroDisplay?: "auto" | "stripIfInteger"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Export the options type to avoid d.ts build errors.
currencyFormatter is exported but its parameter references a non-exported interface; this breaks builds when declaration: true.
Apply:
-interface CurrencyFormatterOptions extends Intl.NumberFormatOptions {
+export interface CurrencyFormatterOptions extends Intl.NumberFormatOptions {
trailingZeroDisplay?: "auto" | "stripIfInteger";
}📝 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.
| interface CurrencyFormatterOptions extends Intl.NumberFormatOptions { | |
| trailingZeroDisplay?: "auto" | "stripIfInteger"; | |
| } | |
| export interface CurrencyFormatterOptions extends Intl.NumberFormatOptions { | |
| trailingZeroDisplay?: "auto" | "stripIfInteger"; | |
| } |
🤖 Prompt for AI Agents
packages/utils/src/functions/currency-formatter.ts lines 1-3: the
CurrencyFormatterOptions interface is not exported but is referenced by the
exported currencyFormatter API which breaks declaration builds; fix by exporting
the type (add the export keyword before the interface or convert it to an
exported type alias) so the options type is part of the public surface, and run
the d.ts build to verify no other internal-only types are referenced.
Summary by CodeRabbit
Style
Refactor