diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx index 0619c28e..17188e9d 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx @@ -1,9 +1,7 @@ "use client"; import React from "react"; -import { Formatting } from "@customer-portal/domain/toolkit"; - -const { formatCurrency } = Formatting; +import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; interface InvoiceTotalsProps { subtotal: number; @@ -12,7 +10,7 @@ interface InvoiceTotalsProps { } export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) { - const fmt = (amount: number) => formatCurrency(amount); + const { formatCurrency } = useFormatCurrency(); return (
@@ -22,13 +20,13 @@ export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
Subtotal - {fmt(subtotal)} + {formatCurrency(subtotal)}
{tax > 0 && (
Tax - {fmt(tax)} + {formatCurrency(tax)}
)} @@ -36,7 +34,7 @@ export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
Total Amount
-
{fmt(total)}
+
{formatCurrency(total)}
JPY
diff --git a/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx b/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx index abeb647b..2a1fbef3 100644 --- a/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx +++ b/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx @@ -3,9 +3,8 @@ import Link from "next/link"; import { CalendarDaysIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; import { format, formatDistanceToNow } from "date-fns"; -import { Formatting } from "@customer-portal/domain/toolkit"; -const { formatCurrency, getCurrencyLocale } = Formatting; +import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; interface UpcomingPaymentBannerProps { invoice: { id: number; amount: number; currency?: string; dueDate: string }; @@ -14,6 +13,8 @@ interface UpcomingPaymentBannerProps { } export function UpcomingPaymentBanner({ invoice, onPay, loading }: UpcomingPaymentBannerProps) { + const { formatCurrency } = useFormatCurrency(); + return (
diff --git a/apps/portal/src/features/dashboard/views/DashboardView.tsx b/apps/portal/src/features/dashboard/views/DashboardView.tsx index a73cdcd4..b0521ff9 100644 --- a/apps/portal/src/features/dashboard/views/DashboardView.tsx +++ b/apps/portal/src/features/dashboard/views/DashboardView.tsx @@ -27,11 +27,12 @@ import { LoadingStats, LoadingTable } from "@/components/atoms"; import { ErrorState } from "@/components/atoms/error-state"; import { Formatting } from "@customer-portal/domain/toolkit"; -const { formatCurrency, getCurrencyLocale } = Formatting; +import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; import { log } from "@customer-portal/logging"; import { useCreateInvoiceSsoLink } from "@/features/billing/hooks/useBilling"; export function DashboardView() { + const { formatCurrency } = useFormatCurrency(); const router = useRouter(); const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore(); diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx index f5b22bed..06961dea 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx @@ -15,10 +15,9 @@ import { import { StatusPill } from "@/components/atoms/status-pill"; import { Button } from "@/components/atoms/button"; import { SubCard } from "@/components/molecules/SubCard/SubCard"; -import { Formatting } from "@customer-portal/domain/toolkit"; import type { Subscription } from "@customer-portal/domain/subscriptions"; -const { formatCurrency, getCurrencyLocale } = Formatting; +import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; import { cn } from "@/lib/utils"; interface SubscriptionCardProps { @@ -78,6 +77,8 @@ const getBillingCycleLabel = (cycle: string) => { export const SubscriptionCard = forwardRef( ({ subscription, variant = "list", showActions = true, onViewClick, className }, ref) => { + const { formatCurrency } = useFormatCurrency(); + const handleViewClick = () => { if (onViewClick) { onViewClick(subscription); diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx index 54ea5c85..478ad2e9 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx @@ -15,10 +15,9 @@ import { } from "@heroicons/react/24/outline"; import { StatusPill } from "@/components/atoms/status-pill"; import { SubCard } from "@/components/molecules/SubCard/SubCard"; -import { Formatting } from "@customer-portal/domain/toolkit"; import type { Subscription } from "@customer-portal/domain/subscriptions"; -const { formatCurrency, getCurrencyLocale } = Formatting; +import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; import { cn } from "@/lib/utils"; interface SubscriptionDetailsProps { @@ -104,6 +103,8 @@ const isVpnService = (productName: string) => { export const SubscriptionDetails = forwardRef( ({ subscription, showServiceSpecificSections = true, className }, ref) => { + const { formatCurrency } = useFormatCurrency(); + return (
{/* Main Details Card */} diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index e54b8bb3..ff3176df 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -26,10 +26,11 @@ import { useSubscriptions, useSubscriptionStats } from "@/features/subscriptions import { Formatting } from "@customer-portal/domain/toolkit"; import type { Subscription } from "@customer-portal/domain/subscriptions"; -const { formatCurrency, getCurrencyLocale } = Formatting; +import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; export function SubscriptionsListContainer() { const router = useRouter(); + const { formatCurrency } = useFormatCurrency(); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); diff --git a/apps/portal/src/lib/hooks/useFormatCurrency.ts b/apps/portal/src/lib/hooks/useFormatCurrency.ts index 95df1e70..183906a0 100644 --- a/apps/portal/src/lib/hooks/useFormatCurrency.ts +++ b/apps/portal/src/lib/hooks/useFormatCurrency.ts @@ -4,7 +4,7 @@ import { useCurrency } from "@/lib/hooks/useCurrency"; import { formatCurrency as baseFormatCurrency } from "@customer-portal/domain/toolkit"; export function useFormatCurrency() { - const { currencyCode, loading, error } = useCurrency(); + const { currencyCode, currencySymbol, loading, error } = useCurrency(); const formatCurrency = (amount: number) => { if (loading) { @@ -14,11 +14,11 @@ export function useFormatCurrency() { if (error) { // Fallback to JPY if there's an error - return baseFormatCurrency(amount, "JPY"); + return baseFormatCurrency(amount, "JPY", "¥"); } // Use the currency from WHMCS API - return baseFormatCurrency(amount, currencyCode); + return baseFormatCurrency(amount, currencyCode, currencySymbol); }; return { diff --git a/apps/portal/src/lib/services/currency.service.ts b/apps/portal/src/lib/services/currency.service.ts index 90730b12..58141cd0 100644 --- a/apps/portal/src/lib/services/currency.service.ts +++ b/apps/portal/src/lib/services/currency.service.ts @@ -19,7 +19,7 @@ class CurrencyServiceImpl implements CurrencyService { if (!response.data) { throw new Error("Failed to get default currency"); } - return response.data; + return response.data as CurrencyInfo; } async getAllCurrencies(): Promise { @@ -27,7 +27,7 @@ class CurrencyServiceImpl implements CurrencyService { if (!response.data) { throw new Error("Failed to get currencies"); } - return response.data; + return response.data as CurrencyInfo[]; } } diff --git a/packages/domain/toolkit/formatting/currency.ts b/packages/domain/toolkit/formatting/currency.ts index 8d5b7fb1..aec4d22a 100644 --- a/packages/domain/toolkit/formatting/currency.ts +++ b/packages/domain/toolkit/formatting/currency.ts @@ -8,36 +8,33 @@ export type SupportedCurrency = "JPY" | "USD" | "EUR"; /** - * Format a number as currency using WHMCS default currency + * Format a number as currency using WHMCS currency data * * @param amount - The numeric amount to format - * @param currencyCode - Optional currency code (defaults to WHMCS default) - * @param locale - Optional locale (defaults to currency-specific locale) + * @param currencyCode - Currency code from WHMCS API (e.g., "JPY", "USD", "EUR") + * @param currencyPrefix - Currency symbol from WHMCS API (e.g., "¥", "$", "€") * * @example - * formatCurrency(1000) // Uses WHMCS default currency - * formatCurrency(1000, "USD") // Uses specific currency - * formatCurrency(1000, "JPY", "ja-JP") // Uses specific currency and locale + * formatCurrency(1000, "JPY", "¥") // ¥1,000 + * formatCurrency(1000, "USD", "$") // $1,000.00 + * formatCurrency(1000, "EUR", "€") // €1,000.00 */ export function formatCurrency( amount: number, - currencyCode: string = "JPY", - locale?: string + currencyCode: string, + currencyPrefix: string ): string { - // Use provided locale or get from currency - const currencyLocale = locale || getCurrencyLocale(currencyCode as SupportedCurrency); - // Determine fraction digits based on currency const fractionDigits = currencyCode === "JPY" ? 0 : 2; - - const formatter = new Intl.NumberFormat(currencyLocale, { - style: "currency", - currency: currencyCode, + + // Format the number with appropriate decimal places + const formattedAmount = amount.toLocaleString("en-US", { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits, }); - return formatter.format(amount); + // Add currency prefix + return `${currencyPrefix}${formattedAmount}`; } /** @@ -50,15 +47,3 @@ export function parseCurrency(value: string): number | null { return Number.isFinite(parsed) ? parsed : null; } -/** - * Get the locale string for a given currency - */ -export function getCurrencyLocale(currency: SupportedCurrency = "JPY"): string { - const localeMap: Record = { - JPY: "ja-JP", - USD: "en-US", - EUR: "de-DE", - }; - return localeMap[currency] || "en-US"; -} - diff --git a/packages/domain/toolkit/index.ts b/packages/domain/toolkit/index.ts index 365d983c..bf171701 100644 --- a/packages/domain/toolkit/index.ts +++ b/packages/domain/toolkit/index.ts @@ -9,7 +9,7 @@ export * as Validation from "./validation/index"; export * as Typing from "./typing/index"; // Re-export commonly used utilities for convenience -export { formatCurrency, getCurrencyLocale } from "./formatting/currency"; +export { formatCurrency } from "./formatting/currency"; export type { SupportedCurrency } from "./formatting/currency"; // Re-export AsyncState types and helpers