@burdenoff/microfe-billing 2026.826.4 → 2026.828.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/shared/ui/StatusPill.js.map +1 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddonsBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonsBrowsePage.tsx"],"sourcesContent":["/**\n * Addons Module - Addons Browse Page (User-facing)\n * Browse available addons and add to cart. No create/edit/toggle actions.\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { useDefaultBillingCurrency } from '../../../hooks/useDefaultBillingCurrency';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { ShoppingCart, Plus, Minus, Trash2, X, Package } from 'lucide-react';\nimport { useAddons } from '../hooks';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\n\ntype ViewMode = 'grid' | 'list';\n\n// ---------------------------------------------------------------------------\n// Grouping logic\n// Addons named \"Group — Variant\" (em dash) are grouped together.\n// Addons with a unique prefix or no em dash are shown standalone.\n// ---------------------------------------------------------------------------\n\ntype AddonGroupItem =\n | { type: 'group'; name: string; addons: Addon[] }\n | { type: 'standalone'; addon: Addon };\n\nfunction groupAddons(addons: Addon[]): AddonGroupItem[] {\n const groups = new Map<string, Addon[]>();\n for (const addon of addons) {\n const sepIdx = addon.name.indexOf(' — ');\n const key = sepIdx !== -1 ? addon.name.slice(0, sepIdx).trim() : addon.name;\n const bucket = groups.get(key) ?? [];\n bucket.push(addon);\n groups.set(key, bucket);\n }\n\n const items: AddonGroupItem[] = [];\n for (const [name, groupAddons] of groups) {\n if (groupAddons.length > 1) {\n items.push({ type: 'group', name, addons: groupAddons });\n } else {\n items.push({ type: 'standalone', addon: groupAddons[0] });\n }\n }\n return items;\n}\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nfunction getAddonDisplayPrice(\n addon: Addon,\n displayCurrency: string = 'USD'\n): { price: number; currency: string } {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const currencyPricesObj = addon.currencyPrices as Record<string, number>;\n if (\n currencyPricesObj[displayCurrency] !== undefined &&\n currencyPricesObj[displayCurrency] !== null\n ) {\n return { price: currencyPricesObj[displayCurrency], currency: displayCurrency };\n }\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n}\n\nexport const AddonsBrowsePage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const { productId } = useBilling();\n const { addons, isLoading, error, refetch } = useAddons({\n includeFree: false,\n productId: productId ?? undefined,\n });\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n const displayCurrency = useDefaultBillingCurrency();\n\n // Only show active addons to users\n const filteredAddons = addons.filter((addon) => {\n if (!addon.isActive) return false;\n if (billingInterval !== 'all' && addon.duration !== billingInterval) return false;\n return true;\n });\n\n const groupedItems = useMemo(() => groupAddons(filteredAddons), [filteredAddons]);\n\n if (isLoading && addons.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.addons.failedToLoad', 'Failed to load addons')}\n </h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n {tr('billing.addons.title', 'Addons')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.addons.subtitle', 'Enhance your subscription with additional features')}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.addons.browsePurpose',\n 'Add extra capacity or capabilities on top of your current plan — like more seats, storage or usage credits — without upgrading your whole subscription. Add the add-ons you want to your cart and check out when ready.'\n )}\n </PagePurpose>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n {(['all', 'monthly', 'yearly'] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval as PlanDuration | 'all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === interval\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {interval === 'all' ? 'All' : interval === 'monthly' ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Addons */}\n {filteredAddons.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr('billing.addons.noAddonsAvailable', 'No addons available')}\n description={tr('billing.addons.checkBackLater', 'Check back later for new addons.')}\n />\n ) : (\n <div className=\"space-y-8\">\n {groupedItems.map((item) => {\n if (item.type === 'group') {\n return (\n <AddonGroupSection\n key={item.name}\n groupName={item.name}\n addons={item.addons}\n viewMode={viewMode}\n getCartItem={getCartItem}\n isInCart={isInCart}\n addToCart={addToCart}\n updateQuantity={updateQuantity}\n removeFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n navigateTo={navigateTo}\n />\n );\n }\n const addon = item.addon;\n const cartItem = getCartItem(addon.id);\n return viewMode === 'grid' ? (\n <BrowseAddonCard\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n ) : (\n <BrowseAddonRow\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n )}\n\n <FloatingCartDrawer displayCurrency={displayCurrency} />\n </div>\n );\n};\n\n// ============================================================================\n// Addon Group Section\n// ============================================================================\n\ninterface AddonGroupSectionProps {\n groupName: string;\n addons: Addon[];\n viewMode: ViewMode;\n getCartItem: (id: string) => { quantity: number } | undefined;\n isInCart: (id: string) => boolean;\n addToCart: (addon: Addon) => void;\n updateQuantity: (id: string, qty: number) => void;\n removeFromCart: (id: string) => void;\n displayCurrency: string;\n navigateTo: (path: string) => void;\n}\n\nconst AddonGroupSection: FC<AddonGroupSectionProps> = ({\n groupName,\n addons,\n viewMode,\n getCartItem,\n isInCart,\n addToCart,\n updateQuantity,\n removeFromCart,\n displayCurrency,\n navigateTo,\n}) => {\n const cartCount = addons.filter((a) => isInCart(a.id)).length;\n\n return (\n <div className=\"border border-border-subtle rounded-xl overflow-hidden\">\n {/* Group header */}\n <div className=\"flex items-center justify-between px-5 py-3 bg-bg-sunken/40 border-b border-border-subtle\">\n <div className=\"flex items-center gap-2\">\n <Package className=\"size-4 text-text-secondary\" />\n <h2 className=\"font-semibold text-text-primary\">{groupName}</h2>\n <span className=\"text-xs text-text-secondary\">\n {addons.length} option{addons.length !== 1 ? 's' : ''}\n </span>\n </div>\n {cartCount > 0 && (\n <span className=\"text-xs font-medium px-2 py-0.5 bg-action-primary-bg text-action-primary-text rounded-full\">\n {cartCount} in cart\n </span>\n )}\n </div>\n\n {/* Variants */}\n {viewMode === 'grid' ? (\n <div className=\"p-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`relative flex flex-col rounded-lg border p-4 transition-all ${\n inCart\n ? 'border-primary bg-action-primary-bg/5'\n : 'border-border-subtle bg-bg-surface hover:border-primary/40 hover:shadow-sm'\n }`}\n >\n {inCart && (\n <span className=\"absolute top-2 right-2 text-xs font-medium bg-action-primary-bg text-action-primary-text px-1.5 py-0.5 rounded\">\n ×{qty}\n </span>\n )}\n <p className=\"font-semibold text-text-primary text-sm mb-1\">{variantName}</p>\n <p className=\"text-xs text-text-secondary mb-3\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n <p className=\"text-lg font-bold text-text-primary mb-3\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n <div className=\"mt-auto flex flex-col gap-2\">\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"w-full py-1.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-1\"\n >\n <ShoppingCart className=\"size-3\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"w-full py-1.5 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"divide-y divide-border\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`flex items-center gap-4 px-5 py-3 transition-colors ${\n inCart ? 'bg-action-primary-bg/5' : 'hover:bg-bg-sunken/30'\n }`}\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium text-text-primary text-sm\">{variantName}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n </div>\n <p className=\"font-semibold text-text-primary text-sm whitespace-nowrap\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1\"\n >\n <ShoppingCart className=\"size-3\" />\n Add\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"px-2 py-1 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Card (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonCardProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst BrowseAddonCard: FC<BrowseAddonCardProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`relative border rounded-lg p-6 transition-all hover:shadow-md flex flex-col h-full ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n {isInCart && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center mb-4\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-text-primary\">{addon.name}</h3>\n <span className=\"text-sm text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-text-secondary text-sm\">/ {formatPlanDuration(addon.duration)}</span>\n </div>\n\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 4).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-text-primary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 4 && (\n <li className=\"text-sm text-text-secondary pl-6\">\n +{combinedQuotas.length - 4} more quotas\n </li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n {isInCart ? (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View Details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Row (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonRowProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst BrowseAddonRow: FC<BrowseAddonRowProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-lg transition-all hover:shadow-sm ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary truncate\">{addon.name}</h3>\n {isInCart && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-secondary\">per {formatPlanDuration(addon.duration)}</div>\n </div>\n\n {isInCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors ml-1\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1.5\"\n >\n <ShoppingCart className=\"size-4\" />\n Add\n </button>\n )}\n\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View\n </button>\n </div>\n );\n};\n\n// ============================================================================\n// Floating Cart Drawer\n// ============================================================================\n\nconst FloatingCartDrawer: FC<{ displayCurrency: string }> = ({ displayCurrency }) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const { items, isCartOpen, toggleCart, closeCart, removeFromCart, updateQuantity, clearCart } =\n useAddonCartStore();\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n\n if (items.length === 0 && !isCartOpen) return null;\n\n return (\n <>\n {!isCartOpen && items.length > 0 && (\n <button\n type=\"button\"\n onClick={toggleCart}\n className=\"fixed bottom-6 right-6 z-50 flex items-center gap-2 px-4 py-3 bg-action-primary-bg text-action-primary-text rounded-full shadow-lg hover:bg-action-primary-bg/90 transition-all\"\n >\n <ShoppingCart className=\"size-5\" />\n <span className=\"font-medium\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n <span className=\"px-2 py-0.5 bg-primary-foreground/20 rounded-full text-sm\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </button>\n )}\n\n {isCartOpen && (\n <div className=\"fixed inset-0 z-50 flex items-end sm:items-center justify-center\">\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={closeCart} />\n <div className=\"relative w-full max-w-md max-h-[80vh] bg-bg-surface border border-border-subtle rounded-t-xl sm:rounded-xl shadow-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-subtle bg-bg-sunken/30\">\n <div className=\"flex items-center gap-2\">\n <ShoppingCart className=\"size-5 text-text-primary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.yourCart', 'Your Cart')}\n </h3>\n <span className=\"px-2 py-0.5 text-xs bg-action-primary-bg text-action-primary-text rounded-full\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={closeCart}\n className=\"p-1.5 rounded hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n <div className=\"overflow-y-auto max-h-[50vh] divide-y divide-border\">\n {items.map(({ addon, quantity }) => (\n <div key={addon.id} className=\"p-4 flex items-center gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-text-primary truncate\">{addon.name}</h4>\n <p className=\"text-sm text-text-secondary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}{' '}\n / {formatPlanDuration(addon.duration)}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{quantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n </div>\n <p className=\"font-medium text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price * quantity, currency);\n })()}\n </p>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n\n <div className=\"border-t border-border-subtle p-4 space-y-3 bg-bg-sunken/20\">\n <div className=\"flex items-center justify-between text-lg font-semibold\">\n <span className=\"text-text-primary\">{tr('billing.addons.total', 'Total')}</span>\n <span className=\"text-text-primary\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Clear Cart\n </button>\n <button\n type=\"button\"\n onClick={() => {\n closeCart();\n navigateTo('/checkout/addons');\n }}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Checkout\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AddonsBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonsBrowsePage.tsx"],"sourcesContent":["/**\n * Addons Module - Addons Browse Page (User-facing)\n * Browse available addons and add to cart. No create/edit/toggle actions.\n */\n\nimport { useState, useMemo, type FC } from 'react';\nimport { useDefaultBillingCurrency } from '../../../hooks/useDefaultBillingCurrency';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { ShoppingCart, Plus, Minus, Trash2, X, Package } from 'lucide-react';\nimport { useAddons } from '../hooks';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport type { Addon, PlanDuration, SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\n\ntype ViewMode = 'grid' | 'list';\n\n// ---------------------------------------------------------------------------\n// Grouping logic\n// Addons named \"Group — Variant\" (em dash) are grouped together.\n// Addons with a unique prefix or no em dash are shown standalone.\n// ---------------------------------------------------------------------------\n\ntype AddonGroupItem =\n { type: 'group'; name: string; addons: Addon[] } | { type: 'standalone'; addon: Addon };\n\nfunction groupAddons(addons: Addon[]): AddonGroupItem[] {\n const groups = new Map<string, Addon[]>();\n for (const addon of addons) {\n const sepIdx = addon.name.indexOf(' — ');\n const key = sepIdx !== -1 ? addon.name.slice(0, sepIdx).trim() : addon.name;\n const bucket = groups.get(key) ?? [];\n bucket.push(addon);\n groups.set(key, bucket);\n }\n\n const items: AddonGroupItem[] = [];\n for (const [name, groupAddons] of groups) {\n if (groupAddons.length > 1) {\n items.push({ type: 'group', name, addons: groupAddons });\n } else {\n items.push({ type: 'standalone', addon: groupAddons[0] });\n }\n }\n return items;\n}\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nfunction getAddonDisplayPrice(\n addon: Addon,\n displayCurrency: string = 'USD'\n): { price: number; currency: string } {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const currencyPricesObj = addon.currencyPrices as Record<string, number>;\n if (\n currencyPricesObj[displayCurrency] !== undefined &&\n currencyPricesObj[displayCurrency] !== null\n ) {\n return { price: currencyPricesObj[displayCurrency], currency: displayCurrency };\n }\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n}\n\nexport const AddonsBrowsePage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const { productId } = useBilling();\n const { addons, isLoading, error, refetch } = useAddons({\n includeFree: false,\n productId: productId ?? undefined,\n });\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n const displayCurrency = useDefaultBillingCurrency();\n\n // Only show active addons to users\n const filteredAddons = addons.filter((addon) => {\n if (!addon.isActive) return false;\n if (billingInterval !== 'all' && addon.duration !== billingInterval) return false;\n return true;\n });\n\n const groupedItems = useMemo(() => groupAddons(filteredAddons), [filteredAddons]);\n\n if (isLoading && addons.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.addons.failedToLoad', 'Failed to load addons')}\n </h2>\n <p className=\"text-sm text-text-secondary\">{error.message}</p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Try Again\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n {tr('billing.addons.title', 'Addons')}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr('billing.addons.subtitle', 'Enhance your subscription with additional features')}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.addons.browsePurpose',\n 'Add extra capacity or capabilities on top of your current plan — like more seats, storage or usage credits — without upgrading your whole subscription. Add the add-ons you want to your cart and check out when ready.'\n )}\n </PagePurpose>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n {(['all', 'monthly', 'yearly'] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval as PlanDuration | 'all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === interval\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {interval === 'all' ? 'All' : interval === 'monthly' ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Addons */}\n {filteredAddons.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr('billing.addons.noAddonsAvailable', 'No addons available')}\n description={tr('billing.addons.checkBackLater', 'Check back later for new addons.')}\n />\n ) : (\n <div className=\"space-y-8\">\n {groupedItems.map((item) => {\n if (item.type === 'group') {\n return (\n <AddonGroupSection\n key={item.name}\n groupName={item.name}\n addons={item.addons}\n viewMode={viewMode}\n getCartItem={getCartItem}\n isInCart={isInCart}\n addToCart={addToCart}\n updateQuantity={updateQuantity}\n removeFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n navigateTo={navigateTo}\n />\n );\n }\n const addon = item.addon;\n const cartItem = getCartItem(addon.id);\n return viewMode === 'grid' ? (\n <BrowseAddonCard\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n ) : (\n <BrowseAddonRow\n key={addon.id}\n addon={addon}\n onView={() => navigateTo(`/addons/${addon.id}`)}\n isInCart={isInCart(addon.id)}\n cartQuantity={cartItem?.quantity || 0}\n onAddToCart={addToCart}\n onUpdateQuantity={updateQuantity}\n onRemoveFromCart={removeFromCart}\n displayCurrency={displayCurrency}\n />\n );\n })}\n </div>\n )}\n\n <FloatingCartDrawer displayCurrency={displayCurrency} />\n </div>\n );\n};\n\n// ============================================================================\n// Addon Group Section\n// ============================================================================\n\ninterface AddonGroupSectionProps {\n groupName: string;\n addons: Addon[];\n viewMode: ViewMode;\n getCartItem: (id: string) => { quantity: number } | undefined;\n isInCart: (id: string) => boolean;\n addToCart: (addon: Addon) => void;\n updateQuantity: (id: string, qty: number) => void;\n removeFromCart: (id: string) => void;\n displayCurrency: string;\n navigateTo: (path: string) => void;\n}\n\nconst AddonGroupSection: FC<AddonGroupSectionProps> = ({\n groupName,\n addons,\n viewMode,\n getCartItem,\n isInCart,\n addToCart,\n updateQuantity,\n removeFromCart,\n displayCurrency,\n navigateTo,\n}) => {\n const cartCount = addons.filter((a) => isInCart(a.id)).length;\n\n return (\n <div className=\"border border-border-subtle rounded-xl overflow-hidden\">\n {/* Group header */}\n <div className=\"flex items-center justify-between px-5 py-3 bg-bg-sunken/40 border-b border-border-subtle\">\n <div className=\"flex items-center gap-2\">\n <Package className=\"size-4 text-text-secondary\" />\n <h2 className=\"font-semibold text-text-primary\">{groupName}</h2>\n <span className=\"text-xs text-text-secondary\">\n {addons.length} option{addons.length !== 1 ? 's' : ''}\n </span>\n </div>\n {cartCount > 0 && (\n <span className=\"text-xs font-medium px-2 py-0.5 bg-action-primary-bg text-action-primary-text rounded-full\">\n {cartCount} in cart\n </span>\n )}\n </div>\n\n {/* Variants */}\n {viewMode === 'grid' ? (\n <div className=\"p-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`relative flex flex-col rounded-lg border p-4 transition-all ${\n inCart\n ? 'border-primary bg-action-primary-bg/5'\n : 'border-border-subtle bg-bg-surface hover:border-primary/40 hover:shadow-sm'\n }`}\n >\n {inCart && (\n <span className=\"absolute top-2 right-2 text-xs font-medium bg-action-primary-bg text-action-primary-text px-1.5 py-0.5 rounded\">\n ×{qty}\n </span>\n )}\n <p className=\"font-semibold text-text-primary text-sm mb-1\">{variantName}</p>\n <p className=\"text-xs text-text-secondary mb-3\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n <p className=\"text-lg font-bold text-text-primary mb-3\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n <div className=\"mt-auto flex flex-col gap-2\">\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"w-full py-1.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-1\"\n >\n <ShoppingCart className=\"size-3\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"w-full py-1.5 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"divide-y divide-border\">\n {addons.map((addon) => {\n const cartItem = getCartItem(addon.id);\n const variantName = addon.name.includes(' — ')\n ? addon.name.slice(addon.name.indexOf(' — ') + 3).trim()\n : addon.name;\n const inCart = isInCart(addon.id);\n const qty = cartItem?.quantity ?? 0;\n const { price, currency } = (() => {\n if (addon.currencyPrices && typeof addon.currencyPrices === 'object') {\n const prices = addon.currencyPrices as Record<string, number>;\n if (prices[displayCurrency] != null)\n return { price: prices[displayCurrency], currency: displayCurrency };\n }\n return { price: addon.price, currency: addon.currency || 'USD' };\n })();\n\n return (\n <div\n key={addon.id}\n className={`flex items-center gap-4 px-5 py-3 transition-colors ${\n inCart ? 'bg-action-primary-bg/5' : 'hover:bg-bg-sunken/30'\n }`}\n >\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium text-text-primary text-sm\">{variantName}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </p>\n </div>\n <p className=\"font-semibold text-text-primary text-sm whitespace-nowrap\">\n {formatCurrency(price, currency)}\n <span className=\"text-xs font-normal text-text-secondary ml-1\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </p>\n {inCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{qty}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, qty + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-3\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1\"\n >\n <ShoppingCart className=\"size-3\" />\n Add\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/addons/${addon.id}`)}\n className=\"px-2 py-1 text-xs font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Details\n </button>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Card (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonCardProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst BrowseAddonCard: FC<BrowseAddonCardProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`relative border rounded-lg p-6 transition-all hover:shadow-md flex flex-col h-full ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n {isInCart && (\n <span className=\"absolute top-4 right-4 px-2 py-1 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center mb-4\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-text-primary\">{addon.name}</h3>\n <span className=\"text-sm text-text-secondary\">\n {formatPlanDurationLabel(addon.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-text-secondary text-sm\">/ {formatPlanDuration(addon.duration)}</span>\n </div>\n\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 4).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-text-primary\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 4 && (\n <li className=\"text-sm text-text-secondary pl-6\">\n +{combinedQuotas.length - 4} more quotas\n </li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border-subtle mt-auto\">\n {isInCart ? (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n )}\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View Details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Addon Row (no edit/toggle)\n// ============================================================================\n\ninterface BrowseAddonRowProps {\n addon: Addon;\n onView: () => void;\n isInCart: boolean;\n cartQuantity: number;\n onAddToCart: (addon: Addon) => void;\n onUpdateQuantity: (addonId: string, quantity: number) => void;\n onRemoveFromCart: (addonId: string) => void;\n displayCurrency: string;\n}\n\nconst BrowseAddonRow: FC<BrowseAddonRowProps> = ({\n addon,\n onView,\n isInCart,\n cartQuantity,\n onAddToCart,\n onUpdateQuantity,\n onRemoveFromCart,\n displayCurrency,\n}) => {\n const combinedQuotas = useMemo(() => combineQuotas(addon.features || []), [addon.features]);\n\n return (\n <div\n className={`flex items-center gap-4 p-4 border rounded-lg transition-all hover:shadow-sm ${\n isInCart ? 'border-primary bg-action-primary-bg/5' : 'border-border-subtle bg-bg-surface'\n }`}\n >\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-text-primary truncate\">{addon.name}</h3>\n {isInCart && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-action-primary-bg text-action-primary-text rounded\">\n In Cart ({cartQuantity})\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''}\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-text-secondary\">per {formatPlanDuration(addon.duration)}</div>\n </div>\n\n {isInCart ? (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-8 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => onUpdateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => onRemoveFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors ml-1\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => onAddToCart(addon)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center gap-1.5\"\n >\n <ShoppingCart className=\"size-4\" />\n Add\n </button>\n )}\n\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n View\n </button>\n </div>\n );\n};\n\n// ============================================================================\n// Floating Cart Drawer\n// ============================================================================\n\nconst FloatingCartDrawer: FC<{ displayCurrency: string }> = ({ displayCurrency }) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const navigateTo = useBillingNavigate();\n const { items, isCartOpen, toggleCart, closeCart, removeFromCart, updateQuantity, clearCart } =\n useAddonCartStore();\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n\n if (items.length === 0 && !isCartOpen) return null;\n\n return (\n <>\n {!isCartOpen && items.length > 0 && (\n <button\n type=\"button\"\n onClick={toggleCart}\n className=\"fixed bottom-6 right-6 z-50 flex items-center gap-2 px-4 py-3 bg-action-primary-bg text-action-primary-text rounded-full shadow-lg hover:bg-action-primary-bg/90 transition-all\"\n >\n <ShoppingCart className=\"size-5\" />\n <span className=\"font-medium\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n <span className=\"px-2 py-0.5 bg-primary-foreground/20 rounded-full text-sm\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </button>\n )}\n\n {isCartOpen && (\n <div className=\"fixed inset-0 z-50 flex items-end sm:items-center justify-center\">\n <div className=\"absolute inset-0 bg-overlay-scrim\" onClick={closeCart} />\n <div className=\"relative w-full max-w-md max-h-[80vh] bg-bg-surface border border-border-subtle rounded-t-xl sm:rounded-xl shadow-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-subtle bg-bg-sunken/30\">\n <div className=\"flex items-center gap-2\">\n <ShoppingCart className=\"size-5 text-text-primary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.yourCart', 'Your Cart')}\n </h3>\n <span className=\"px-2 py-0.5 text-xs bg-action-primary-bg text-action-primary-text rounded-full\">\n {itemCount} item{itemCount !== 1 ? 's' : ''}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={closeCart}\n className=\"p-1.5 rounded hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n\n <div className=\"overflow-y-auto max-h-[50vh] divide-y divide-border\">\n {items.map(({ addon, quantity }) => (\n <div key={addon.id} className=\"p-4 flex items-center gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-text-primary truncate\">{addon.name}</h4>\n <p className=\"text-sm text-text-secondary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price, currency);\n })()}{' '}\n / {formatPlanDuration(addon.duration)}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity - 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-3\" />\n </button>\n <span className=\"w-6 text-center text-sm font-medium\">{quantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, quantity + 1)}\n className=\"p-1 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-3\" />\n </button>\n </div>\n <p className=\"font-medium text-text-primary\">\n {(() => {\n const { price, currency } = getAddonDisplayPrice(addon, displayCurrency);\n return formatCurrency(price * quantity, currency);\n })()}\n </p>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n\n <div className=\"border-t border-border-subtle p-4 space-y-3 bg-bg-sunken/20\">\n <div className=\"flex items-center justify-between text-lg font-semibold\">\n <span className=\"text-text-primary\">{tr('billing.addons.total', 'Total')}</span>\n <span className=\"text-text-primary\">\n {(() => {\n const total = items.reduce((sum, { addon, quantity }) => {\n const { price } = getAddonDisplayPrice(addon, displayCurrency);\n return sum + price * quantity;\n }, 0);\n return formatCurrency(total, displayCurrency);\n })()}\n </span>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={clearCart}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Clear Cart\n </button>\n <button\n type=\"button\"\n onClick={() => {\n closeCart();\n navigateTo('/checkout/addons');\n }}\n className=\"flex-1 px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Checkout\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRefunds.js","names":[],"sources":["../../../../../src/billing/modules/billing/hooks/useRefunds.ts"],"sourcesContent":["/**\n * Billing Module - useRefunds Hook\n * Provides access to refund records, statistics, and mutations\n */\n\nimport { useApolloClient } from '@apollo/client/react';\nimport { useCallback, useState, useMemo, useEffect } from 'react';\nimport {\n useGetRefundRecordsQuery,\n useGetPendingRefundsQuery,\n useGetRefundRecordQuery,\n useGetRefundStatsQuery,\n useGetUnprocessedFailedWebhooksQuery,\n useCreateRefundRecordMutation,\n useProcessRefundMutation,\n useSkipRefundMutation,\n useRetryRefundMutation,\n useCreateRefundRecordsFromFailedWebhooksMutation,\n useCreateRefundRecordFromWebhookMutation,\n GetRefundRecordsDocument,\n GetPendingRefundsDocument,\n GetRefundStatsDocument,\n GetUnprocessedFailedWebhooksDocument,\n} from '../../../../generated/global-operations';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type RefundRecordStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'SKIPPED';\nexport type RefundSource =\n | 'MANUAL'\n | 'WEBHOOK_RECOVERY'\n | 'SUBSCRIPTION_CANCEL'\n | 'DUPLICATE_PAYMENT';\n\nexport interface RefundRecord {\n id: string;\n orgId: string;\n billingAccountId: string | null;\n source: RefundSource;\n status: RefundRecordStatus;\n provider: string;\n gatewayPaymentId: string;\n gatewayOrderId: string | null;\n amount: number;\n currency: string;\n purchaseType: string | null;\n purchaseDetail: string | null;\n reason: string | null;\n notes: string | null;\n webhookLogId: string | null;\n transactionId: string | null;\n processedBy: string | null;\n processedAt: string | null;\n gatewayRefundId: string | null;\n errorMessage: string | null;\n retryCount: number;\n metadata: Record<string, unknown> | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface RefundStats {\n pending: number;\n processing: number;\n completed: number;\n failed: number;\n skipped: number;\n totalPendingAmount: number;\n totalCompletedAmount: number;\n}\n\nexport interface WebhookLog {\n id: string;\n event: string;\n provider: string;\n payload: Record<string, unknown>;\n processed: boolean;\n error: string | null;\n createdAt: string;\n}\n\nexport interface RefundRecordFilters {\n status?: RefundRecordStatus;\n source?: RefundSource;\n provider?: string;\n billingAccountId?: string;\n fromDate?: string;\n toDate?: string;\n}\n\nexport interface CreateRefundRecordInput {\n billingAccountId?: string;\n provider: string;\n gatewayPaymentId: string;\n gatewayOrderId?: string;\n amount: number;\n currency: string;\n purchaseType?: string;\n purchaseDetail?: string;\n reason?: string;\n notes?: string;\n transactionId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface ProcessRefundResult {\n success: boolean;\n refundRecord: RefundRecord | null;\n gatewayRefundId: string | null;\n error: string | null;\n}\n\ninterface PaginationInfo {\n totalCount: number;\n hasMore: boolean;\n page: number;\n pageSize: number;\n hasPrevious: boolean;\n}\n\n// ============================================================================\n// Hook Results\n// ============================================================================\n\ninterface UseRefundRecordsOptions {\n pageSize?: number;\n filters?: RefundRecordFilters;\n}\n\ninterface UseRefundRecordsResult {\n records: RefundRecord[];\n pagination: PaginationInfo;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n goToNextPage: () => void;\n goToPreviousPage: () => void;\n goToPage: (page: number) => void;\n setFilters: (filters: RefundRecordFilters) => void;\n}\n\ninterface UseRefundStatsResult {\n stats: RefundStats | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseRefundRecordResult {\n record: RefundRecord | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseUnprocessedWebhooksResult {\n webhooks: WebhookLog[];\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseRefundMutationsResult {\n createRefundRecord: (input: CreateRefundRecordInput) => Promise<RefundRecord>;\n processRefund: (id: string, notes?: string) => Promise<ProcessRefundResult>;\n skipRefund: (id: string, reason: string) => Promise<RefundRecord>;\n retryRefund: (id: string) => Promise<ProcessRefundResult>;\n createRefundRecordsFromFailedWebhooks: () => Promise<RefundRecord[]>;\n createRefundRecordFromWebhook: (webhookLogId: string) => Promise<RefundRecord | null>;\n isCreating: boolean;\n isProcessing: boolean;\n isSkipping: boolean;\n isRetrying: boolean;\n isCreatingFromWebhooks: boolean;\n isCreatingFromWebhook: boolean;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nconst DEFAULT_PAGE_SIZE = 20;\n\n/**\n * Hook for fetching paginated refund records with filters\n */\nexport function useRefundRecords(options: UseRefundRecordsOptions = {}): UseRefundRecordsResult {\n const pageSize = options.pageSize || DEFAULT_PAGE_SIZE;\n const [currentPage, setCurrentPage] = useState(0);\n const [filters, setFilters] = useState<RefundRecordFilters>(options.filters || {});\n\n // Update filters when options change (e.g., when tab changes)\n useEffect(() => {\n setFilters(options.filters || {});\n setCurrentPage(0); // Reset to first page when filters change externally\n }, [options.filters]);\n\n const { data, loading, error, refetch } = useGetRefundRecordsQuery({\n variables: {\n ...filters,\n page: currentPage,\n pageSize,\n },\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const result = data?.getRefundRecords;\n\n const goToNextPage = useCallback(() => {\n if (result?.hasMore) {\n setCurrentPage((prev) => prev + 1);\n }\n }, [result?.hasMore]);\n\n const goToPreviousPage = useCallback(() => {\n if (currentPage > 0) {\n setCurrentPage((prev) => prev - 1);\n }\n }, [currentPage]);\n\n const goToPage = useCallback(\n (page: number) => {\n const totalPages = result ? Math.ceil(result.totalCount / pageSize) : 0;\n if (page >= 0 && page < totalPages) {\n setCurrentPage(page);\n }\n },\n [result, pageSize]\n );\n\n const handleSetFilters = useCallback((newFilters: RefundRecordFilters) => {\n setFilters(newFilters);\n setCurrentPage(0); // Reset to first page when filters change\n }, []);\n\n return {\n records: result?.records || [],\n pagination: {\n totalCount: result?.totalCount || 0,\n hasMore: result?.hasMore || false,\n page: currentPage,\n pageSize,\n hasPrevious: currentPage > 0,\n },\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n goToNextPage,\n goToPreviousPage,\n goToPage,\n setFilters: handleSetFilters,\n };\n}\n\n/**\n * Hook for fetching pending refund records (shortcut)\n */\nexport function usePendingRefunds(pageSize = DEFAULT_PAGE_SIZE): UseRefundRecordsResult {\n const [currentPage, setCurrentPage] = useState(0);\n\n const { data, loading, error, refetch } = useGetPendingRefundsQuery({\n variables: { page: currentPage, pageSize },\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const result = data?.getPendingRefunds;\n\n const goToNextPage = useCallback(() => {\n if (result?.hasMore) {\n setCurrentPage((prev) => prev + 1);\n }\n }, [result?.hasMore]);\n\n const goToPreviousPage = useCallback(() => {\n if (currentPage > 0) {\n setCurrentPage((prev) => prev - 1);\n }\n }, [currentPage]);\n\n const goToPage = useCallback(\n (page: number) => {\n const totalPages = result ? Math.ceil(result.totalCount / pageSize) : 0;\n if (page >= 0 && page < totalPages) {\n setCurrentPage(page);\n }\n },\n [result, pageSize]\n );\n\n return {\n records: result?.records || [],\n pagination: {\n totalCount: result?.totalCount || 0,\n hasMore: result?.hasMore || false,\n page: currentPage,\n pageSize,\n hasPrevious: currentPage > 0,\n },\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n goToNextPage,\n goToPreviousPage,\n goToPage,\n setFilters: () => {}, // No-op for pending refunds\n };\n}\n\n/**\n * Hook for fetching refund statistics\n */\nexport function useRefundStats(): UseRefundStatsResult {\n const { data, loading, error, refetch } = useGetRefundStatsQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n stats: data?.getRefundStats || null,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching a single refund record by ID\n */\nexport function useRefundRecord(id: string | undefined): UseRefundRecordResult {\n const { data, loading, error, refetch } = useGetRefundRecordQuery({\n variables: { id: id! },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n record: data?.getRefundRecord || null,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching unprocessed failed webhooks\n */\nexport function useUnprocessedFailedWebhooks(): UseUnprocessedWebhooksResult {\n const { data, loading, error, refetch } = useGetUnprocessedFailedWebhooksQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n webhooks: data?.getUnprocessedFailedWebhooks || [],\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for refund mutations\n */\nexport function useRefundMutations(): UseRefundMutationsResult {\n const client = useApolloClient();\n\n const [createRefundRecordMutation, { loading: isCreating }] = useCreateRefundRecordMutation();\n const [processRefundMutation, { loading: isProcessing }] = useProcessRefundMutation();\n const [skipRefundMutation, { loading: isSkipping }] = useSkipRefundMutation();\n const [retryRefundMutation, { loading: isRetrying }] = useRetryRefundMutation();\n const [createFromWebhooksMutation, { loading: isCreatingFromWebhooks }] =\n useCreateRefundRecordsFromFailedWebhooksMutation();\n const [createFromWebhookMutation, { loading: isCreatingFromWebhook }] =\n useCreateRefundRecordFromWebhookMutation();\n\n const refetchQueries = useCallback(async () => {\n await client.refetchQueries({\n include: [GetRefundRecordsDocument, GetPendingRefundsDocument, GetRefundStatsDocument],\n });\n }, [client]);\n\n const createRefundRecord = useCallback(\n async (input: CreateRefundRecordInput): Promise<RefundRecord> => {\n const { data } = await createRefundRecordMutation({\n variables: input,\n });\n await refetchQueries();\n return data.createRefundRecord;\n },\n [createRefundRecordMutation, refetchQueries]\n );\n\n const processRefund = useCallback(\n async (id: string, notes?: string): Promise<ProcessRefundResult> => {\n const { data } = await processRefundMutation({\n variables: { id, notes },\n });\n await refetchQueries();\n return data.processRefund;\n },\n [processRefundMutation, refetchQueries]\n );\n\n const skipRefund = useCallback(\n async (id: string, reason: string): Promise<RefundRecord> => {\n const { data } = await skipRefundMutation({\n variables: { id, reason },\n });\n await refetchQueries();\n return data.skipRefund;\n },\n [skipRefundMutation, refetchQueries]\n );\n\n const retryRefund = useCallback(\n async (id: string): Promise<ProcessRefundResult> => {\n const { data } = await retryRefundMutation({\n variables: { id },\n });\n await refetchQueries();\n return data.retryRefund;\n },\n [retryRefundMutation, refetchQueries]\n );\n\n const createRefundRecordsFromFailedWebhooks = useCallback(async (): Promise<RefundRecord[]> => {\n const { data } = await createFromWebhooksMutation();\n await client.refetchQueries({\n include: [\n GetRefundRecordsDocument,\n GetPendingRefundsDocument,\n GetRefundStatsDocument,\n GetUnprocessedFailedWebhooksDocument,\n ],\n });\n return data!.createRefundRecordsFromFailedWebhooks;\n }, [createFromWebhooksMutation, client]);\n\n const createRefundRecordFromWebhook = useCallback(\n async (webhookLogId: string): Promise<RefundRecord | null> => {\n const { data } = await createFromWebhookMutation({\n variables: { webhookLogId },\n });\n await client.refetchQueries({\n include: [\n GetRefundRecordsDocument,\n GetPendingRefundsDocument,\n GetRefundStatsDocument,\n GetUnprocessedFailedWebhooksDocument,\n ],\n });\n return data!.createRefundRecordFromWebhook;\n },\n [createFromWebhookMutation, client]\n );\n\n return {\n createRefundRecord,\n processRefund,\n skipRefund,\n retryRefund,\n createRefundRecordsFromFailedWebhooks,\n createRefundRecordFromWebhook,\n isCreating,\n isProcessing,\n isSkipping,\n isRetrying,\n isCreatingFromWebhooks,\n isCreatingFromWebhook,\n };\n}\n\n/**\n * Combined hook for refund page with all data and mutations\n */\nexport function useRefunds(options: UseRefundRecordsOptions = {}) {\n const recordsHook = useRefundRecords(options);\n const statsHook = useRefundStats();\n const mutationsHook = useRefundMutations();\n const webhooksHook = useUnprocessedFailedWebhooks();\n\n const isLoading = useMemo(\n () => recordsHook.isLoading || statsHook.isLoading,\n [recordsHook.isLoading, statsHook.isLoading]\n );\n\n const refetchAll = useCallback(async () => {\n await Promise.all([recordsHook.refetch(), statsHook.refetch(), webhooksHook.refetch()]);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentional: accessing stable refetch methods from hooks\n }, [recordsHook.refetch, statsHook.refetch, webhooksHook.refetch]);\n\n return {\n // Records\n records: recordsHook.records,\n pagination: recordsHook.pagination,\n goToNextPage: recordsHook.goToNextPage,\n goToPreviousPage: recordsHook.goToPreviousPage,\n goToPage: recordsHook.goToPage,\n setFilters: recordsHook.setFilters,\n\n // Stats\n stats: statsHook.stats,\n\n // Failed webhooks\n unprocessedWebhooks: webhooksHook.webhooks,\n webhooksLoading: webhooksHook.isLoading,\n\n // Loading states\n isLoading,\n recordsError: recordsHook.error,\n statsError: statsHook.error,\n\n // Mutations\n ...mutationsHook,\n\n // Refetch\n refetch: refetchAll,\n refetchRecords: recordsHook.refetch,\n refetchStats: statsHook.refetch,\n refetchWebhooks: webhooksHook.refetch,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useRefunds.js","names":[],"sources":["../../../../../src/billing/modules/billing/hooks/useRefunds.ts"],"sourcesContent":["/**\n * Billing Module - useRefunds Hook\n * Provides access to refund records, statistics, and mutations\n */\n\nimport { useApolloClient } from '@apollo/client/react';\nimport { useCallback, useState, useMemo, useEffect } from 'react';\nimport {\n useGetRefundRecordsQuery,\n useGetPendingRefundsQuery,\n useGetRefundRecordQuery,\n useGetRefundStatsQuery,\n useGetUnprocessedFailedWebhooksQuery,\n useCreateRefundRecordMutation,\n useProcessRefundMutation,\n useSkipRefundMutation,\n useRetryRefundMutation,\n useCreateRefundRecordsFromFailedWebhooksMutation,\n useCreateRefundRecordFromWebhookMutation,\n GetRefundRecordsDocument,\n GetPendingRefundsDocument,\n GetRefundStatsDocument,\n GetUnprocessedFailedWebhooksDocument,\n} from '../../../../generated/global-operations';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type RefundRecordStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'SKIPPED';\nexport type RefundSource =\n 'MANUAL' | 'WEBHOOK_RECOVERY' | 'SUBSCRIPTION_CANCEL' | 'DUPLICATE_PAYMENT';\n\nexport interface RefundRecord {\n id: string;\n orgId: string;\n billingAccountId: string | null;\n source: RefundSource;\n status: RefundRecordStatus;\n provider: string;\n gatewayPaymentId: string;\n gatewayOrderId: string | null;\n amount: number;\n currency: string;\n purchaseType: string | null;\n purchaseDetail: string | null;\n reason: string | null;\n notes: string | null;\n webhookLogId: string | null;\n transactionId: string | null;\n processedBy: string | null;\n processedAt: string | null;\n gatewayRefundId: string | null;\n errorMessage: string | null;\n retryCount: number;\n metadata: Record<string, unknown> | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface RefundStats {\n pending: number;\n processing: number;\n completed: number;\n failed: number;\n skipped: number;\n totalPendingAmount: number;\n totalCompletedAmount: number;\n}\n\nexport interface WebhookLog {\n id: string;\n event: string;\n provider: string;\n payload: Record<string, unknown>;\n processed: boolean;\n error: string | null;\n createdAt: string;\n}\n\nexport interface RefundRecordFilters {\n status?: RefundRecordStatus;\n source?: RefundSource;\n provider?: string;\n billingAccountId?: string;\n fromDate?: string;\n toDate?: string;\n}\n\nexport interface CreateRefundRecordInput {\n billingAccountId?: string;\n provider: string;\n gatewayPaymentId: string;\n gatewayOrderId?: string;\n amount: number;\n currency: string;\n purchaseType?: string;\n purchaseDetail?: string;\n reason?: string;\n notes?: string;\n transactionId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface ProcessRefundResult {\n success: boolean;\n refundRecord: RefundRecord | null;\n gatewayRefundId: string | null;\n error: string | null;\n}\n\ninterface PaginationInfo {\n totalCount: number;\n hasMore: boolean;\n page: number;\n pageSize: number;\n hasPrevious: boolean;\n}\n\n// ============================================================================\n// Hook Results\n// ============================================================================\n\ninterface UseRefundRecordsOptions {\n pageSize?: number;\n filters?: RefundRecordFilters;\n}\n\ninterface UseRefundRecordsResult {\n records: RefundRecord[];\n pagination: PaginationInfo;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n goToNextPage: () => void;\n goToPreviousPage: () => void;\n goToPage: (page: number) => void;\n setFilters: (filters: RefundRecordFilters) => void;\n}\n\ninterface UseRefundStatsResult {\n stats: RefundStats | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseRefundRecordResult {\n record: RefundRecord | null;\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseUnprocessedWebhooksResult {\n webhooks: WebhookLog[];\n isLoading: boolean;\n error: Error | undefined;\n refetch: () => Promise<void>;\n}\n\ninterface UseRefundMutationsResult {\n createRefundRecord: (input: CreateRefundRecordInput) => Promise<RefundRecord>;\n processRefund: (id: string, notes?: string) => Promise<ProcessRefundResult>;\n skipRefund: (id: string, reason: string) => Promise<RefundRecord>;\n retryRefund: (id: string) => Promise<ProcessRefundResult>;\n createRefundRecordsFromFailedWebhooks: () => Promise<RefundRecord[]>;\n createRefundRecordFromWebhook: (webhookLogId: string) => Promise<RefundRecord | null>;\n isCreating: boolean;\n isProcessing: boolean;\n isSkipping: boolean;\n isRetrying: boolean;\n isCreatingFromWebhooks: boolean;\n isCreatingFromWebhook: boolean;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nconst DEFAULT_PAGE_SIZE = 20;\n\n/**\n * Hook for fetching paginated refund records with filters\n */\nexport function useRefundRecords(options: UseRefundRecordsOptions = {}): UseRefundRecordsResult {\n const pageSize = options.pageSize || DEFAULT_PAGE_SIZE;\n const [currentPage, setCurrentPage] = useState(0);\n const [filters, setFilters] = useState<RefundRecordFilters>(options.filters || {});\n\n // Update filters when options change (e.g., when tab changes)\n useEffect(() => {\n setFilters(options.filters || {});\n setCurrentPage(0); // Reset to first page when filters change externally\n }, [options.filters]);\n\n const { data, loading, error, refetch } = useGetRefundRecordsQuery({\n variables: {\n ...filters,\n page: currentPage,\n pageSize,\n },\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const result = data?.getRefundRecords;\n\n const goToNextPage = useCallback(() => {\n if (result?.hasMore) {\n setCurrentPage((prev) => prev + 1);\n }\n }, [result?.hasMore]);\n\n const goToPreviousPage = useCallback(() => {\n if (currentPage > 0) {\n setCurrentPage((prev) => prev - 1);\n }\n }, [currentPage]);\n\n const goToPage = useCallback(\n (page: number) => {\n const totalPages = result ? Math.ceil(result.totalCount / pageSize) : 0;\n if (page >= 0 && page < totalPages) {\n setCurrentPage(page);\n }\n },\n [result, pageSize]\n );\n\n const handleSetFilters = useCallback((newFilters: RefundRecordFilters) => {\n setFilters(newFilters);\n setCurrentPage(0); // Reset to first page when filters change\n }, []);\n\n return {\n records: result?.records || [],\n pagination: {\n totalCount: result?.totalCount || 0,\n hasMore: result?.hasMore || false,\n page: currentPage,\n pageSize,\n hasPrevious: currentPage > 0,\n },\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n goToNextPage,\n goToPreviousPage,\n goToPage,\n setFilters: handleSetFilters,\n };\n}\n\n/**\n * Hook for fetching pending refund records (shortcut)\n */\nexport function usePendingRefunds(pageSize = DEFAULT_PAGE_SIZE): UseRefundRecordsResult {\n const [currentPage, setCurrentPage] = useState(0);\n\n const { data, loading, error, refetch } = useGetPendingRefundsQuery({\n variables: { page: currentPage, pageSize },\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n const result = data?.getPendingRefunds;\n\n const goToNextPage = useCallback(() => {\n if (result?.hasMore) {\n setCurrentPage((prev) => prev + 1);\n }\n }, [result?.hasMore]);\n\n const goToPreviousPage = useCallback(() => {\n if (currentPage > 0) {\n setCurrentPage((prev) => prev - 1);\n }\n }, [currentPage]);\n\n const goToPage = useCallback(\n (page: number) => {\n const totalPages = result ? Math.ceil(result.totalCount / pageSize) : 0;\n if (page >= 0 && page < totalPages) {\n setCurrentPage(page);\n }\n },\n [result, pageSize]\n );\n\n return {\n records: result?.records || [],\n pagination: {\n totalCount: result?.totalCount || 0,\n hasMore: result?.hasMore || false,\n page: currentPage,\n pageSize,\n hasPrevious: currentPage > 0,\n },\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n goToNextPage,\n goToPreviousPage,\n goToPage,\n setFilters: () => {}, // No-op for pending refunds\n };\n}\n\n/**\n * Hook for fetching refund statistics\n */\nexport function useRefundStats(): UseRefundStatsResult {\n const { data, loading, error, refetch } = useGetRefundStatsQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n stats: data?.getRefundStats || null,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching a single refund record by ID\n */\nexport function useRefundRecord(id: string | undefined): UseRefundRecordResult {\n const { data, loading, error, refetch } = useGetRefundRecordQuery({\n variables: { id: id! },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n record: data?.getRefundRecord || null,\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for fetching unprocessed failed webhooks\n */\nexport function useUnprocessedFailedWebhooks(): UseUnprocessedWebhooksResult {\n const { data, loading, error, refetch } = useGetUnprocessedFailedWebhooksQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n const handleRefetch = useCallback(async () => {\n await refetch();\n }, [refetch]);\n\n return {\n webhooks: data?.getUnprocessedFailedWebhooks || [],\n isLoading: loading,\n error: error as Error | undefined,\n refetch: handleRefetch,\n };\n}\n\n/**\n * Hook for refund mutations\n */\nexport function useRefundMutations(): UseRefundMutationsResult {\n const client = useApolloClient();\n\n const [createRefundRecordMutation, { loading: isCreating }] = useCreateRefundRecordMutation();\n const [processRefundMutation, { loading: isProcessing }] = useProcessRefundMutation();\n const [skipRefundMutation, { loading: isSkipping }] = useSkipRefundMutation();\n const [retryRefundMutation, { loading: isRetrying }] = useRetryRefundMutation();\n const [createFromWebhooksMutation, { loading: isCreatingFromWebhooks }] =\n useCreateRefundRecordsFromFailedWebhooksMutation();\n const [createFromWebhookMutation, { loading: isCreatingFromWebhook }] =\n useCreateRefundRecordFromWebhookMutation();\n\n const refetchQueries = useCallback(async () => {\n await client.refetchQueries({\n include: [GetRefundRecordsDocument, GetPendingRefundsDocument, GetRefundStatsDocument],\n });\n }, [client]);\n\n const createRefundRecord = useCallback(\n async (input: CreateRefundRecordInput): Promise<RefundRecord> => {\n const { data } = await createRefundRecordMutation({\n variables: input,\n });\n await refetchQueries();\n return data.createRefundRecord;\n },\n [createRefundRecordMutation, refetchQueries]\n );\n\n const processRefund = useCallback(\n async (id: string, notes?: string): Promise<ProcessRefundResult> => {\n const { data } = await processRefundMutation({\n variables: { id, notes },\n });\n await refetchQueries();\n return data.processRefund;\n },\n [processRefundMutation, refetchQueries]\n );\n\n const skipRefund = useCallback(\n async (id: string, reason: string): Promise<RefundRecord> => {\n const { data } = await skipRefundMutation({\n variables: { id, reason },\n });\n await refetchQueries();\n return data.skipRefund;\n },\n [skipRefundMutation, refetchQueries]\n );\n\n const retryRefund = useCallback(\n async (id: string): Promise<ProcessRefundResult> => {\n const { data } = await retryRefundMutation({\n variables: { id },\n });\n await refetchQueries();\n return data.retryRefund;\n },\n [retryRefundMutation, refetchQueries]\n );\n\n const createRefundRecordsFromFailedWebhooks = useCallback(async (): Promise<RefundRecord[]> => {\n const { data } = await createFromWebhooksMutation();\n await client.refetchQueries({\n include: [\n GetRefundRecordsDocument,\n GetPendingRefundsDocument,\n GetRefundStatsDocument,\n GetUnprocessedFailedWebhooksDocument,\n ],\n });\n return data!.createRefundRecordsFromFailedWebhooks;\n }, [createFromWebhooksMutation, client]);\n\n const createRefundRecordFromWebhook = useCallback(\n async (webhookLogId: string): Promise<RefundRecord | null> => {\n const { data } = await createFromWebhookMutation({\n variables: { webhookLogId },\n });\n await client.refetchQueries({\n include: [\n GetRefundRecordsDocument,\n GetPendingRefundsDocument,\n GetRefundStatsDocument,\n GetUnprocessedFailedWebhooksDocument,\n ],\n });\n return data!.createRefundRecordFromWebhook;\n },\n [createFromWebhookMutation, client]\n );\n\n return {\n createRefundRecord,\n processRefund,\n skipRefund,\n retryRefund,\n createRefundRecordsFromFailedWebhooks,\n createRefundRecordFromWebhook,\n isCreating,\n isProcessing,\n isSkipping,\n isRetrying,\n isCreatingFromWebhooks,\n isCreatingFromWebhook,\n };\n}\n\n/**\n * Combined hook for refund page with all data and mutations\n */\nexport function useRefunds(options: UseRefundRecordsOptions = {}) {\n const recordsHook = useRefundRecords(options);\n const statsHook = useRefundStats();\n const mutationsHook = useRefundMutations();\n const webhooksHook = useUnprocessedFailedWebhooks();\n\n const isLoading = useMemo(\n () => recordsHook.isLoading || statsHook.isLoading,\n [recordsHook.isLoading, statsHook.isLoading]\n );\n\n const refetchAll = useCallback(async () => {\n await Promise.all([recordsHook.refetch(), statsHook.refetch(), webhooksHook.refetch()]);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentional: accessing stable refetch methods from hooks\n }, [recordsHook.refetch, statsHook.refetch, webhooksHook.refetch]);\n\n return {\n // Records\n records: recordsHook.records,\n pagination: recordsHook.pagination,\n goToNextPage: recordsHook.goToNextPage,\n goToPreviousPage: recordsHook.goToPreviousPage,\n goToPage: recordsHook.goToPage,\n setFilters: recordsHook.setFilters,\n\n // Stats\n stats: statsHook.stats,\n\n // Failed webhooks\n unprocessedWebhooks: webhooksHook.webhooks,\n webhooksLoading: webhooksHook.isLoading,\n\n // Loading states\n isLoading,\n recordsError: recordsHook.error,\n statsError: statsHook.error,\n\n // Mutations\n ...mutationsHook,\n\n // Refetch\n refetch: refetchAll,\n refetchRecords: recordsHook.refetch,\n refetchStats: statsHook.refetch,\n refetchWebhooks: webhooksHook.refetch,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCheckout.js","names":[],"sources":["../../../../../src/billing/modules/checkout/hooks/useCheckout.ts"],"sourcesContent":["/**\n * Main checkout hook for managing checkout state and payment processing\n */\n\nimport { useState, useCallback, useMemo, useEffect } from 'react';\nimport {\n useInitiatePaymentCheckoutMutation,\n useSpendCreditsCheckoutMutation,\n useConvertCurrencyLazyQuery,\n useValidateCouponLazyQuery,\n useCreateBillingOrderCheckoutMutation,\n} from '../../../../generated/global-operations';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport type {\n CheckoutItem,\n CheckoutBillingAccount,\n PaymentProvider,\n Currency,\n CheckoutState,\n PaymentInitResponse,\n PromoCodeResult,\n PurchaseType,\n} from '../types';\n\n/**\n * Credit conversion rate: How many credits equal 1 USD\n * This should match the backend creditRate.rateUSD (default 0.01 means 100 credits = $1)\n */\nconst CREDITS_PER_USD = 100;\n\ninterface UseCheckoutOptions {\n billingAccountId?: string;\n defaultCurrency?: Currency;\n defaultSaveCard?: boolean;\n upgradeFromSubscriptionId?: string;\n /** For plan downgrades — old sub auto-renewal cancelled, runs to natural expiry */\n downgradeFromSubscriptionId?: string;\n /** For addon purchases - pre-selected subscription ID */\n defaultSubscriptionId?: string;\n}\n\ninterface UseCheckoutResult {\n // State\n items: CheckoutItem[];\n billingAccount: CheckoutBillingAccount | null;\n selectedProvider: PaymentProvider;\n selectedCurrency: Currency;\n promoCode: string;\n promoDiscount: number;\n promoDiscountType: 'PERCENTAGE' | 'FIXED';\n useCredits: boolean;\n creditsToUse: number;\n checkoutState: CheckoutState;\n error: string | null;\n\n // Saved card state\n selectedPaymentMethodId: string | null;\n saveCard: boolean;\n\n // Auto-pay state (for subscription purchases)\n autoRenew: boolean;\n autoRenewPaymentMethodId: string | null;\n\n // Subscription state (for addon purchases)\n selectedSubscriptionId: string | null;\n\n // Computed values\n subtotal: number;\n subtotalUSD: number;\n discountAmount: number;\n taxAmount: number;\n taxRate: number;\n taxType: string;\n total: number;\n totalInSelectedCurrency: number;\n availableCredits: number;\n hasEnoughCredits: boolean;\n canPayWithCredits: boolean;\n isLoading: boolean;\n isProcessing: boolean;\n isCalculatingTax: boolean;\n\n // Currency conversion\n conversionRate: number;\n convertedAmount: number | null;\n\n // Actions\n addItem: (item: CheckoutItem) => void;\n removeItem: (itemId: string) => void;\n updateItemQuantity: (itemId: string, quantity: number) => void;\n updateItemPrice: (itemId: string, price: number, currency?: Currency) => void;\n clearItems: () => void;\n setBillingAccount: (account: CheckoutBillingAccount | null) => void;\n setPaymentProvider: (provider: PaymentProvider) => void;\n setCurrency: (currency: Currency) => void;\n setPromoCode: (code: string) => void;\n validatePromoCode: () => Promise<PromoCodeResult>;\n clearPromoCode: () => void;\n setUseCredits: (use: boolean) => void;\n setCreditsToUse: (amount: number) => void;\n setSelectedPaymentMethodId: (id: string | null) => void;\n setSaveCard: (save: boolean) => void;\n setAutoRenew: (enabled: boolean) => void;\n setAutoRenewPaymentMethodId: (id: string | null) => void;\n setSelectedSubscriptionId: (id: string | null) => void;\n initiatePayment: () => Promise<PaymentInitResponse>;\n reset: () => void;\n}\n\nexport function useCheckout(options: UseCheckoutOptions = {}): UseCheckoutResult {\n const { currentUser } = useBilling();\n const {\n defaultCurrency = 'USD',\n defaultSaveCard = false, // CRITICAL: Never save cards without explicit user consent\n upgradeFromSubscriptionId,\n downgradeFromSubscriptionId,\n defaultSubscriptionId,\n } = options;\n\n // Core state\n const [items, setItems] = useState<CheckoutItem[]>([]);\n const [billingAccount, setBillingAccount] = useState<CheckoutBillingAccount | null>(null);\n const [selectedProvider, setSelectedProvider] = useState<PaymentProvider>('AUTO');\n const [selectedCurrency, setSelectedCurrency] = useState<Currency>(defaultCurrency);\n const [promoCode, setPromoCode] = useState('');\n const [promoDiscount, setPromoDiscount] = useState(0);\n const [promoDiscountType, setPromoDiscountType] = useState<'PERCENTAGE' | 'FIXED'>('PERCENTAGE');\n const [useCredits, setUseCredits] = useState(false);\n const [creditsToUse, setCreditsToUse] = useState(0);\n const [checkoutState, setCheckoutState] = useState<CheckoutState>('idle');\n const [error, setError] = useState<string | null>(null);\n const [taxAmount, setTaxAmount] = useState(0);\n const [taxRate, setTaxRate] = useState(0);\n const [taxType, setTaxType] = useState<string>('NONE');\n\n // Saved card state\n const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string | null>(null);\n const [saveCard, setSaveCard] = useState(defaultSaveCard);\n\n // Auto-pay state (for subscription purchases)\n const [autoRenew, setAutoRenew] = useState(false);\n const [autoRenewPaymentMethodId, setAutoRenewPaymentMethodId] = useState<string | null>(null);\n\n // Subscription state (for addon purchases)\n const [selectedSubscriptionId, setSelectedSubscriptionId] = useState<string | null>(\n defaultSubscriptionId ?? null\n );\n\n // GraphQL mutations and queries\n const [initiatePaymentMutation] = useInitiatePaymentCheckoutMutation();\n const [createBillingOrderMutation] = useCreateBillingOrderCheckoutMutation();\n const [spendCreditsMutation] = useSpendCreditsCheckoutMutation();\n const [convertCurrency, { data: currencyData, loading: currencyLoading }] =\n useConvertCurrencyLazyQuery();\n const [validateCoupon] = useValidateCouponLazyQuery();\n\n // Computed values\n const subtotal = useMemo(() => {\n return items.reduce((sum, item) => sum + item.price * item.quantity, 0);\n }, [items]);\n\n // USD subtotal — always use priceUSD (base price) for credit calculations.\n // item.price may be in a local currency (INR, EUR, etc.) when currencyPrices is set.\n const subtotalUSD = useMemo(() => {\n return items.reduce((sum, item) => sum + (item.priceUSD ?? item.price) * item.quantity, 0);\n }, [items]);\n\n const discountAmount = useMemo(() => {\n if (!promoDiscount) return 0;\n // promoDiscount now contains the actual discount amount from backend validation\n // (not the percentage value), so use it directly\n return Math.min(promoDiscount, subtotal);\n }, [subtotal, promoDiscount]);\n\n // Total includes tax (calculated via calculateTax query)\n const total = useMemo(() => {\n const afterDiscount = subtotal - discountAmount;\n const withCredits = useCredits ? Math.max(0, afterDiscount - creditsToUse) : afterDiscount;\n return withCredits + taxAmount;\n }, [subtotal, discountAmount, useCredits, creditsToUse, taxAmount]);\n\n const availableCredits = billingAccount?.creditAmount ?? 0;\n\n const hasEnoughCredits = useMemo(() => {\n // Use subtotalUSD so this works correctly for any display currency\n const creditsRequired = subtotalUSD * CREDITS_PER_USD;\n return availableCredits >= creditsRequired;\n }, [availableCredits, subtotalUSD]);\n\n // Can only pay with credits if not buying credits\n const canPayWithCredits = useMemo(() => {\n const hasCreditsPurchase = items.some((item) => item.type === 'CREDITS');\n return !hasCreditsPurchase && availableCredits > 0;\n }, [items, availableCredits]);\n\n // Currency conversion result\n const convertedAmount = currencyData?.convertCurrency?.amount ?? null;\n const conversionRate = currencyData?.convertCurrency?.exchangeRate ?? 1;\n const totalInSelectedCurrency = convertedAmount ?? total;\n\n const isLoading = checkoutState === 'loading' || currencyLoading;\n const isProcessing = checkoutState === 'processing';\n\n // Effect to trigger currency conversion when total or currency changes\n // This ensures the converted amount is always up-to-date\n useEffect(() => {\n // Skip conversion for credit payments (no currency conversion needed)\n if (selectedProvider === 'CREDITS') {\n return;\n }\n\n // Skip if no items yet (cart is still loading)\n if (items.length === 0) {\n return;\n }\n\n // Skip conversion if items already have prices in selected currency\n // (Plans/Addons with multi-currency pricing have currencyPrices stored)\n const allItemsHaveLocalizedPrices = items.every(\n (item) => item.currency === selectedCurrency || item.type === 'CREDITS'\n );\n\n if (allItemsHaveLocalizedPrices) {\n // Items are already priced in the selected currency (from currencyPrices)\n // No conversion needed - subtotal and total are already correct!\n return;\n }\n\n // Only fetch if we have a valid total and non-USD currency\n if (total > 0 && selectedCurrency !== 'USD') {\n // For INR, wait until tax has been calculated (taxAmount > 0 or taxRate is set)\n // This prevents double API calls - one before tax, one after\n if (selectedCurrency === 'INR' && taxAmount === 0 && subtotal > 0) {\n // Tax hasn't been calculated yet, skip this conversion\n // The effect will run again when taxAmount changes\n return;\n }\n\n convertCurrency({\n variables: {\n amount: total,\n toCurrency: selectedCurrency,\n fromCurrency: 'USD',\n },\n });\n }\n }, [total, selectedCurrency, selectedProvider, convertCurrency, taxAmount, subtotal, items]);\n\n // Effect to calculate tax when billing account, subtotal, or currency changes\n // Tax is only applied for INR payments (18% GST)\n useEffect(() => {\n // Only calculate tax if we have a billing account and items\n if (!billingAccount || subtotal <= 0) {\n // Reset tax if no billing account or empty cart\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n return;\n }\n\n // Skip if paying with credits (no tax on credit payments)\n if (selectedProvider === 'CREDITS') {\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n return;\n }\n\n // Only apply tax for INR payments\n if (selectedCurrency !== 'INR') {\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n return;\n }\n\n // For INR: Calculate 18% GST on the USD amount, then we'll convert for display\n const afterDiscount = subtotal - discountAmount;\n const GST_RATE = 0.18;\n const taxInUSD = afterDiscount * GST_RATE;\n\n setTaxAmount(taxInUSD);\n setTaxRate(GST_RATE);\n setTaxType('GST');\n }, [billingAccount, subtotal, discountAmount, selectedCurrency, selectedProvider]);\n\n // Actions\n const addItem = useCallback((item: CheckoutItem) => {\n setItems((prev) => {\n // Plans can only be purchased once - quantity is always 1\n if (item.type === 'PLAN') {\n // Check if any plan already exists in cart\n const existingPlan = prev.find((i) => i.type === 'PLAN');\n if (existingPlan) {\n // Replace existing plan with new one (can only have 1 plan)\n return prev.map((i) => (i.type === 'PLAN' ? { ...item, quantity: 1 } : i));\n }\n // Add new plan with quantity forced to 1\n return [...prev, { ...item, quantity: 1 }];\n }\n\n // For addons and credits, allow quantity increment\n const existing = prev.find((i) => i.id === item.id);\n if (existing) {\n return prev.map((i) =>\n i.id === item.id ? { ...i, quantity: i.quantity + item.quantity } : i\n );\n }\n return [...prev, item];\n });\n }, []);\n\n const removeItem = useCallback((itemId: string) => {\n setItems((prev) => prev.filter((i) => i.id !== itemId));\n }, []);\n\n const updateItemQuantity = useCallback(\n (itemId: string, quantity: number) => {\n if (quantity <= 0) {\n removeItem(itemId);\n return;\n }\n setItems((prev) =>\n prev.map((i) => {\n if (i.id !== itemId) return i;\n // Plans can only have quantity 1\n if (i.type === 'PLAN') {\n return { ...i, quantity: 1 };\n }\n return { ...i, quantity };\n })\n );\n },\n [removeItem]\n );\n\n const updateItemPrice = useCallback((itemId: string, price: number, currency?: Currency) => {\n setItems((prev) =>\n prev.map((i) => {\n if (i.id !== itemId) return i;\n // Update price and optionally currency\n const updates: Partial<CheckoutItem> = { price };\n if (currency) {\n updates.currency = currency;\n }\n return { ...i, ...updates };\n })\n );\n }, []);\n\n const clearItems = useCallback(() => {\n setItems([]);\n }, []);\n\n const handleSetBillingAccount = useCallback((account: CheckoutBillingAccount | null) => {\n setBillingAccount(account);\n // Reset credits usage when billing account changes\n setUseCredits(false);\n setCreditsToUse(0);\n }, []);\n\n const handleSetCurrency = useCallback(\n (currency: Currency) => {\n setSelectedCurrency(currency);\n // Skip conversion if items already have localized prices\n // (Plans/Addons with multi-currency pricing don't need conversion)\n const allItemsHaveLocalizedPrices = items.every(\n (item) => item.currency === currency || item.type === 'CREDITS'\n );\n\n // Only fetch conversion rate if items don't have localized prices\n if (total > 0 && currency !== 'USD' && !allItemsHaveLocalizedPrices) {\n convertCurrency({\n variables: {\n amount: total,\n toCurrency: currency,\n fromCurrency: 'USD',\n },\n });\n }\n },\n [total, convertCurrency, items]\n );\n\n const handleSetPaymentProvider = useCallback(\n (provider: PaymentProvider) => {\n setSelectedProvider(provider);\n // Razorpay only supports INR\n if (provider === 'RAZORPAY' && selectedCurrency !== 'INR') {\n setSelectedCurrency('INR');\n }\n },\n [selectedCurrency]\n );\n\n const validatePromoCode = useCallback(async (): Promise<PromoCodeResult> => {\n if (!promoCode.trim()) {\n return {\n valid: false,\n code: '',\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Please enter a promo code',\n };\n }\n\n try {\n setCheckoutState('loading');\n\n // Build validation input\n const validationInput = {\n code: promoCode.trim().toUpperCase(),\n userId: currentUser?.id ?? '',\n userEmail: currentUser?.email,\n amount: subtotal,\n currency: selectedCurrency,\n };\n\n const { data, error: queryError } = await validateCoupon({\n variables: {\n input: validationInput,\n },\n });\n\n if (queryError || !data?.validateCoupon) {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Failed to validate coupon',\n };\n }\n\n const response = data.validateCoupon;\n\n if (!response.valid) {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: response.error || 'Invalid coupon code',\n };\n }\n\n // Extract discount details from coupon\n const coupon = response.coupon;\n if (!coupon) {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Coupon data not found',\n };\n }\n\n // Use the ACTUAL discount amount from validation (includes caps and currency conversion)\n const actualDiscountAmount = response.discountAmount || 0;\n\n const discountType = coupon.type === 'FIXED_AMOUNT' ? 'FIXED' : 'PERCENTAGE';\n setPromoDiscount(actualDiscountAmount); // Use actual discount, not coupon.value\n setPromoDiscountType(discountType);\n setCheckoutState('idle');\n\n return {\n valid: true,\n code: coupon.code,\n discountType,\n discountValue: actualDiscountAmount, // Use actual discount, not coupon.value\n };\n } catch {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Failed to validate coupon',\n };\n }\n }, [currentUser?.email, currentUser?.id, promoCode, selectedCurrency, subtotal, validateCoupon]);\n\n const clearPromoCode = useCallback(() => {\n setPromoCode('');\n setPromoDiscount(0);\n setPromoDiscountType('PERCENTAGE');\n }, []);\n\n const handleSetCreditsToUse = useCallback(\n (amount: number) => {\n const maxCredits = Math.min(amount, availableCredits, subtotal - discountAmount);\n setCreditsToUse(Math.max(0, maxCredits));\n },\n [availableCredits, subtotal, discountAmount]\n );\n\n const initiatePayment = useCallback(async (): Promise<PaymentInitResponse> => {\n if (!billingAccount) {\n return {\n success: false,\n provider: 'AUTO',\n subtotal: 0,\n taxAmount: 0,\n taxRate: 0,\n total: 0,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: 'Please select a billing account',\n };\n }\n\n if (items.length === 0) {\n return {\n success: false,\n provider: 'AUTO',\n subtotal: 0,\n taxAmount: 0,\n taxRate: 0,\n total: 0,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: 'No items in checkout',\n };\n }\n\n try {\n setCheckoutState('processing');\n setError(null);\n\n // Determine purchase type from first item\n const firstItem = items[0];\n let purchaseType: PurchaseType = firstItem.type;\n\n // Check if this is a store order checkout (detect early)\n const storeOrderItem = items.find((item) => item.metadata?.storeOrderId);\n const storeOrderId = storeOrderItem?.metadata?.storeOrderId as string | undefined;\n let billingOrderId =\n (items.find((item) => item.metadata?.billingOrderId)?.metadata?.billingOrderId as\n | string\n | undefined) || undefined;\n\n // For store orders, use 'ORDER' purchase type\n if (storeOrderId) {\n purchaseType = 'ORDER' as PurchaseType;\n }\n\n // Calculate amount to charge\n const chargeAmount = subtotal - discountAmount;\n\n // Determine provider\n let provider = selectedProvider;\n if (provider === 'AUTO') {\n provider = selectedCurrency === 'INR' ? 'RAZORPAY' : 'STRIPE';\n }\n\n // If this is a store order without a billing order yet, create one now.\n // Pass callbackGraphQL so the billing service can call back to the store service\n // after payment completes to mark the order confirmed.\n if (purchaseType === 'ORDER' && !billingOrderId && storeOrderId && billingAccount) {\n const orderItems = items\n .filter((item) => item.type === 'ORDER')\n .map((item) => ({\n name: item.name,\n description: item.description ?? undefined,\n quantity: item.quantity,\n unitPrice: item.price,\n metadata: {\n productId: item.metadata?.productId ?? undefined,\n variantId: item.metadata?.variantId ?? undefined,\n },\n }));\n\n const { data: billingOrderResult } = await createBillingOrderMutation({\n variables: {\n input: {\n // billingAccountId intentionally not set here — it gets\n // linked at initiatePayment time so the user can switch\n // billing accounts before confirming payment.\n externalOrderId: storeOrderId,\n externalService: 'STORE',\n currency: selectedCurrency,\n subtotal: chargeAmount,\n description: `Store order ${storeOrderId}`,\n items: orderItems,\n callbackGraphQL: {\n endpoint: 'http://global-int-gateway:4002/global/graphql',\n mutationName: 'updateStoreOrderPaymentStatus',\n },\n },\n },\n });\n\n if (billingOrderResult?.createBillingOrder?.success) {\n billingOrderId = billingOrderResult.createBillingOrder.order?.id;\n } else {\n const errMsg =\n billingOrderResult?.createBillingOrder?.error ?? 'Failed to create billing order';\n setCheckoutState('error');\n setError(errMsg);\n return {\n success: false,\n provider,\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: errMsg,\n };\n }\n }\n\n // ========================================\n // CREDITS PAYMENT - use spendCredits mutation\n // Supports multiple addons\n // ========================================\n if (provider === 'CREDITS') {\n const addonItems = items.filter((item) => item.type === 'ADDON');\n const planItem = items.find((item) => item.type === 'PLAN');\n\n const { data } = await spendCreditsMutation({\n variables: {\n input: {\n billingAccountId: billingAccount.id,\n planId: planItem ? (planItem.metadata?.planId as string) || planItem.id : undefined,\n // Multiple addons supported via addOnIds array\n addOnIds:\n addonItems.length > 0\n ? addonItems.map((item) => ({\n id: (item.metadata?.addonId as string) || item.id,\n quantity: item.quantity,\n }))\n : undefined,\n isAnnual: planItem?.metadata?.duration === 'yearly',\n // Include upgrade context if changing plans\n upgradeFromSubscriptionId: upgradeFromSubscriptionId || undefined,\n // Include downgrade context if downgrading\n downgradeFromSubscriptionId: downgradeFromSubscriptionId || undefined,\n // For addon purchases - subscription to attach to\n subscriptionId: addonItems.length > 0 ? selectedSubscriptionId : undefined,\n // For PER_SEAT plans - pass seat count for credit calculation + subscription storage\n seatCount: planItem?.metadata?.seatCount as number | undefined,\n },\n },\n });\n\n const response = data?.spendCredits;\n\n if (!response?.creditTransactionID) {\n setCheckoutState('error');\n setError('Failed to process credit payment');\n return {\n success: false,\n provider: 'CREDITS',\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: 'Failed to process credit payment',\n };\n }\n\n setCheckoutState('success');\n return {\n success: true,\n provider: 'CREDITS',\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n // Return transaction ID for success handling\n orderId: response.creditTransactionID,\n };\n }\n\n // ========================================\n // CARD PAYMENT (Stripe/Razorpay) - use initiatePayment\n // Supports single or multiple addons\n // Supports store orders (via orderId parameter)\n // ========================================\n const addonItems = items.filter((item) => item.type === 'ADDON');\n\n // Build addon IDs and quantities for the mutation\n const addonIds =\n addonItems.length > 0\n ? addonItems.map((item) => (item.metadata?.addonId as string) || item.id)\n : undefined;\n const quantities =\n addonItems.length > 0 ? addonItems.map((item) => item.quantity) : undefined;\n\n // Determine if this is an annual plan purchase\n const planItem = items.find((item) => item.type === 'PLAN');\n const isAnnualPlan = planItem?.metadata?.duration === 'yearly';\n\n // Determine auto-renew payment method\n const autoRenewPMId =\n purchaseType === 'PLAN' && autoRenew\n ? autoRenewPaymentMethodId || selectedPaymentMethodId || undefined\n : undefined;\n\n const { data } = await initiatePaymentMutation({\n variables: {\n input: {\n billingAccountId: billingAccount.id,\n amount: chargeAmount, // Note: Backend IGNORES this and calculates from DB\n currency: selectedCurrency,\n purchaseType,\n provider: provider,\n // Store order - use orderId parameter (billing order ID)\n orderId: billingOrderId || undefined,\n creditAmount: purchaseType === 'CREDITS' ? firstItem.quantity : undefined,\n planId:\n purchaseType === 'PLAN'\n ? (firstItem.metadata?.planId as string) || firstItem.id\n : undefined,\n // Single addon (backwards compatible) - skip if store order\n addonId:\n purchaseType === 'ADDON' && addonItems.length === 1 && !billingOrderId\n ? (addonItems[0].metadata?.addonId as string) || addonItems[0].id\n : undefined,\n quantity:\n purchaseType === 'ADDON' && addonItems.length === 1 && !billingOrderId\n ? addonItems[0].quantity\n : undefined,\n // Multiple addons (cart checkout) - skip if store order\n addonIds:\n purchaseType === 'ADDON' && addonItems.length > 1 && !billingOrderId\n ? addonIds\n : undefined,\n quantities:\n purchaseType === 'ADDON' && addonItems.length > 1 && !billingOrderId\n ? quantities\n : undefined,\n // For addon purchases - subscription to attach to (skip for store orders)\n subscriptionId:\n purchaseType === 'ADDON' && !billingOrderId ? selectedSubscriptionId : undefined,\n // Saved card support\n paymentMethodId: selectedPaymentMethodId || undefined,\n // Save card if: user checked save card OR auto-renewal is enabled (which requires a saved card)\n // Only save if not using existing card (selectedPaymentMethodId is null)\n saveCard: !selectedPaymentMethodId ? saveCard || autoRenew : undefined,\n // Include upgrade context if changing plans\n upgradeFromSubscriptionId: upgradeFromSubscriptionId || undefined,\n // Include downgrade context if downgrading\n downgradeFromSubscriptionId: downgradeFromSubscriptionId || undefined,\n // Annual plan flag (for pricing calculation)\n isAnnual: isAnnualPlan || undefined,\n // Promo code (validated and applied on backend)\n promoCode: promoCode || undefined,\n // Auto-renewal (for plan purchases only)\n autoRenew: purchaseType === 'PLAN' && autoRenew ? true : undefined,\n autoRenewPaymentMethodId: autoRenewPMId,\n },\n },\n });\n\n const response = data?.initiatePayment;\n\n if (!response?.success) {\n setCheckoutState('error');\n setError(response?.error ?? 'Payment initiation failed');\n return {\n success: false,\n provider: provider,\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: response?.error ?? 'Payment initiation failed',\n };\n }\n\n // Note: Tax is already calculated upfront in the effect, don't overwrite\n // The response tax is in converted currency which would mess up display\n setCheckoutState('success');\n\n return response;\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'An error occurred';\n setCheckoutState('error');\n setError(errorMessage);\n return {\n success: false,\n provider: 'AUTO',\n subtotal: 0,\n taxAmount: 0,\n taxRate: 0,\n total: 0,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: errorMessage,\n };\n }\n }, [\n billingAccount,\n items,\n selectedCurrency,\n selectedProvider,\n subtotal,\n discountAmount,\n promoCode,\n selectedPaymentMethodId,\n saveCard,\n autoRenew,\n autoRenewPaymentMethodId,\n selectedSubscriptionId,\n upgradeFromSubscriptionId,\n downgradeFromSubscriptionId,\n initiatePaymentMutation,\n createBillingOrderMutation,\n spendCreditsMutation,\n ]);\n\n const reset = useCallback(() => {\n setItems([]);\n setBillingAccount(null);\n setSelectedProvider('AUTO');\n setSelectedCurrency(defaultCurrency);\n setPromoCode('');\n setPromoDiscount(0);\n setPromoDiscountType('PERCENTAGE');\n setUseCredits(false);\n setCreditsToUse(0);\n setCheckoutState('idle');\n setError(null);\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n setSelectedPaymentMethodId(null);\n setSaveCard(defaultSaveCard);\n setAutoRenew(false);\n setAutoRenewPaymentMethodId(null);\n setSelectedSubscriptionId(defaultSubscriptionId ?? null);\n }, [defaultCurrency, defaultSaveCard, defaultSubscriptionId]);\n\n return {\n // State\n items,\n billingAccount,\n selectedProvider,\n selectedCurrency,\n promoCode,\n promoDiscount,\n promoDiscountType,\n useCredits,\n creditsToUse,\n checkoutState,\n error,\n\n // Saved card state\n selectedPaymentMethodId,\n saveCard,\n\n // Auto-pay state\n autoRenew,\n autoRenewPaymentMethodId,\n\n // Subscription state (for addon purchases)\n selectedSubscriptionId,\n\n // Computed\n subtotal,\n subtotalUSD,\n discountAmount,\n taxAmount,\n taxRate,\n taxType,\n total,\n totalInSelectedCurrency,\n availableCredits,\n hasEnoughCredits,\n canPayWithCredits,\n isLoading,\n isProcessing,\n isCalculatingTax: false, // Tax is calculated synchronously now\n conversionRate,\n convertedAmount,\n\n // Actions\n addItem,\n removeItem,\n updateItemQuantity,\n updateItemPrice,\n clearItems,\n setBillingAccount: handleSetBillingAccount,\n setPaymentProvider: handleSetPaymentProvider,\n setCurrency: handleSetCurrency,\n setPromoCode,\n validatePromoCode,\n clearPromoCode,\n setUseCredits,\n setCreditsToUse: handleSetCreditsToUse,\n setSelectedPaymentMethodId,\n setSaveCard,\n setAutoRenew,\n setAutoRenewPaymentMethodId,\n setSelectedSubscriptionId,\n initiatePayment,\n reset,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useCheckout.js","names":[],"sources":["../../../../../src/billing/modules/checkout/hooks/useCheckout.ts"],"sourcesContent":["/**\n * Main checkout hook for managing checkout state and payment processing\n */\n\nimport { useState, useCallback, useMemo, useEffect } from 'react';\nimport {\n useInitiatePaymentCheckoutMutation,\n useSpendCreditsCheckoutMutation,\n useConvertCurrencyLazyQuery,\n useValidateCouponLazyQuery,\n useCreateBillingOrderCheckoutMutation,\n} from '../../../../generated/global-operations';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport type {\n CheckoutItem,\n CheckoutBillingAccount,\n PaymentProvider,\n Currency,\n CheckoutState,\n PaymentInitResponse,\n PromoCodeResult,\n PurchaseType,\n} from '../types';\n\n/**\n * Credit conversion rate: How many credits equal 1 USD\n * This should match the backend creditRate.rateUSD (default 0.01 means 100 credits = $1)\n */\nconst CREDITS_PER_USD = 100;\n\ninterface UseCheckoutOptions {\n billingAccountId?: string;\n defaultCurrency?: Currency;\n defaultSaveCard?: boolean;\n upgradeFromSubscriptionId?: string;\n /** For plan downgrades — old sub auto-renewal cancelled, runs to natural expiry */\n downgradeFromSubscriptionId?: string;\n /** For addon purchases - pre-selected subscription ID */\n defaultSubscriptionId?: string;\n}\n\ninterface UseCheckoutResult {\n // State\n items: CheckoutItem[];\n billingAccount: CheckoutBillingAccount | null;\n selectedProvider: PaymentProvider;\n selectedCurrency: Currency;\n promoCode: string;\n promoDiscount: number;\n promoDiscountType: 'PERCENTAGE' | 'FIXED';\n useCredits: boolean;\n creditsToUse: number;\n checkoutState: CheckoutState;\n error: string | null;\n\n // Saved card state\n selectedPaymentMethodId: string | null;\n saveCard: boolean;\n\n // Auto-pay state (for subscription purchases)\n autoRenew: boolean;\n autoRenewPaymentMethodId: string | null;\n\n // Subscription state (for addon purchases)\n selectedSubscriptionId: string | null;\n\n // Computed values\n subtotal: number;\n subtotalUSD: number;\n discountAmount: number;\n taxAmount: number;\n taxRate: number;\n taxType: string;\n total: number;\n totalInSelectedCurrency: number;\n availableCredits: number;\n hasEnoughCredits: boolean;\n canPayWithCredits: boolean;\n isLoading: boolean;\n isProcessing: boolean;\n isCalculatingTax: boolean;\n\n // Currency conversion\n conversionRate: number;\n convertedAmount: number | null;\n\n // Actions\n addItem: (item: CheckoutItem) => void;\n removeItem: (itemId: string) => void;\n updateItemQuantity: (itemId: string, quantity: number) => void;\n updateItemPrice: (itemId: string, price: number, currency?: Currency) => void;\n clearItems: () => void;\n setBillingAccount: (account: CheckoutBillingAccount | null) => void;\n setPaymentProvider: (provider: PaymentProvider) => void;\n setCurrency: (currency: Currency) => void;\n setPromoCode: (code: string) => void;\n validatePromoCode: () => Promise<PromoCodeResult>;\n clearPromoCode: () => void;\n setUseCredits: (use: boolean) => void;\n setCreditsToUse: (amount: number) => void;\n setSelectedPaymentMethodId: (id: string | null) => void;\n setSaveCard: (save: boolean) => void;\n setAutoRenew: (enabled: boolean) => void;\n setAutoRenewPaymentMethodId: (id: string | null) => void;\n setSelectedSubscriptionId: (id: string | null) => void;\n initiatePayment: () => Promise<PaymentInitResponse>;\n reset: () => void;\n}\n\nexport function useCheckout(options: UseCheckoutOptions = {}): UseCheckoutResult {\n const { currentUser } = useBilling();\n const {\n defaultCurrency = 'USD',\n defaultSaveCard = false, // CRITICAL: Never save cards without explicit user consent\n upgradeFromSubscriptionId,\n downgradeFromSubscriptionId,\n defaultSubscriptionId,\n } = options;\n\n // Core state\n const [items, setItems] = useState<CheckoutItem[]>([]);\n const [billingAccount, setBillingAccount] = useState<CheckoutBillingAccount | null>(null);\n const [selectedProvider, setSelectedProvider] = useState<PaymentProvider>('AUTO');\n const [selectedCurrency, setSelectedCurrency] = useState<Currency>(defaultCurrency);\n const [promoCode, setPromoCode] = useState('');\n const [promoDiscount, setPromoDiscount] = useState(0);\n const [promoDiscountType, setPromoDiscountType] = useState<'PERCENTAGE' | 'FIXED'>('PERCENTAGE');\n const [useCredits, setUseCredits] = useState(false);\n const [creditsToUse, setCreditsToUse] = useState(0);\n const [checkoutState, setCheckoutState] = useState<CheckoutState>('idle');\n const [error, setError] = useState<string | null>(null);\n const [taxAmount, setTaxAmount] = useState(0);\n const [taxRate, setTaxRate] = useState(0);\n const [taxType, setTaxType] = useState<string>('NONE');\n\n // Saved card state\n const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string | null>(null);\n const [saveCard, setSaveCard] = useState(defaultSaveCard);\n\n // Auto-pay state (for subscription purchases)\n const [autoRenew, setAutoRenew] = useState(false);\n const [autoRenewPaymentMethodId, setAutoRenewPaymentMethodId] = useState<string | null>(null);\n\n // Subscription state (for addon purchases)\n const [selectedSubscriptionId, setSelectedSubscriptionId] = useState<string | null>(\n defaultSubscriptionId ?? null\n );\n\n // GraphQL mutations and queries\n const [initiatePaymentMutation] = useInitiatePaymentCheckoutMutation();\n const [createBillingOrderMutation] = useCreateBillingOrderCheckoutMutation();\n const [spendCreditsMutation] = useSpendCreditsCheckoutMutation();\n const [convertCurrency, { data: currencyData, loading: currencyLoading }] =\n useConvertCurrencyLazyQuery();\n const [validateCoupon] = useValidateCouponLazyQuery();\n\n // Computed values\n const subtotal = useMemo(() => {\n return items.reduce((sum, item) => sum + item.price * item.quantity, 0);\n }, [items]);\n\n // USD subtotal — always use priceUSD (base price) for credit calculations.\n // item.price may be in a local currency (INR, EUR, etc.) when currencyPrices is set.\n const subtotalUSD = useMemo(() => {\n return items.reduce((sum, item) => sum + (item.priceUSD ?? item.price) * item.quantity, 0);\n }, [items]);\n\n const discountAmount = useMemo(() => {\n if (!promoDiscount) return 0;\n // promoDiscount now contains the actual discount amount from backend validation\n // (not the percentage value), so use it directly\n return Math.min(promoDiscount, subtotal);\n }, [subtotal, promoDiscount]);\n\n // Total includes tax (calculated via calculateTax query)\n const total = useMemo(() => {\n const afterDiscount = subtotal - discountAmount;\n const withCredits = useCredits ? Math.max(0, afterDiscount - creditsToUse) : afterDiscount;\n return withCredits + taxAmount;\n }, [subtotal, discountAmount, useCredits, creditsToUse, taxAmount]);\n\n const availableCredits = billingAccount?.creditAmount ?? 0;\n\n const hasEnoughCredits = useMemo(() => {\n // Use subtotalUSD so this works correctly for any display currency\n const creditsRequired = subtotalUSD * CREDITS_PER_USD;\n return availableCredits >= creditsRequired;\n }, [availableCredits, subtotalUSD]);\n\n // Can only pay with credits if not buying credits\n const canPayWithCredits = useMemo(() => {\n const hasCreditsPurchase = items.some((item) => item.type === 'CREDITS');\n return !hasCreditsPurchase && availableCredits > 0;\n }, [items, availableCredits]);\n\n // Currency conversion result\n const convertedAmount = currencyData?.convertCurrency?.amount ?? null;\n const conversionRate = currencyData?.convertCurrency?.exchangeRate ?? 1;\n const totalInSelectedCurrency = convertedAmount ?? total;\n\n const isLoading = checkoutState === 'loading' || currencyLoading;\n const isProcessing = checkoutState === 'processing';\n\n // Effect to trigger currency conversion when total or currency changes\n // This ensures the converted amount is always up-to-date\n useEffect(() => {\n // Skip conversion for credit payments (no currency conversion needed)\n if (selectedProvider === 'CREDITS') {\n return;\n }\n\n // Skip if no items yet (cart is still loading)\n if (items.length === 0) {\n return;\n }\n\n // Skip conversion if items already have prices in selected currency\n // (Plans/Addons with multi-currency pricing have currencyPrices stored)\n const allItemsHaveLocalizedPrices = items.every(\n (item) => item.currency === selectedCurrency || item.type === 'CREDITS'\n );\n\n if (allItemsHaveLocalizedPrices) {\n // Items are already priced in the selected currency (from currencyPrices)\n // No conversion needed - subtotal and total are already correct!\n return;\n }\n\n // Only fetch if we have a valid total and non-USD currency\n if (total > 0 && selectedCurrency !== 'USD') {\n // For INR, wait until tax has been calculated (taxAmount > 0 or taxRate is set)\n // This prevents double API calls - one before tax, one after\n if (selectedCurrency === 'INR' && taxAmount === 0 && subtotal > 0) {\n // Tax hasn't been calculated yet, skip this conversion\n // The effect will run again when taxAmount changes\n return;\n }\n\n convertCurrency({\n variables: {\n amount: total,\n toCurrency: selectedCurrency,\n fromCurrency: 'USD',\n },\n });\n }\n }, [total, selectedCurrency, selectedProvider, convertCurrency, taxAmount, subtotal, items]);\n\n // Effect to calculate tax when billing account, subtotal, or currency changes\n // Tax is only applied for INR payments (18% GST)\n useEffect(() => {\n // Only calculate tax if we have a billing account and items\n if (!billingAccount || subtotal <= 0) {\n // Reset tax if no billing account or empty cart\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n return;\n }\n\n // Skip if paying with credits (no tax on credit payments)\n if (selectedProvider === 'CREDITS') {\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n return;\n }\n\n // Only apply tax for INR payments\n if (selectedCurrency !== 'INR') {\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n return;\n }\n\n // For INR: Calculate 18% GST on the USD amount, then we'll convert for display\n const afterDiscount = subtotal - discountAmount;\n const GST_RATE = 0.18;\n const taxInUSD = afterDiscount * GST_RATE;\n\n setTaxAmount(taxInUSD);\n setTaxRate(GST_RATE);\n setTaxType('GST');\n }, [billingAccount, subtotal, discountAmount, selectedCurrency, selectedProvider]);\n\n // Actions\n const addItem = useCallback((item: CheckoutItem) => {\n setItems((prev) => {\n // Plans can only be purchased once - quantity is always 1\n if (item.type === 'PLAN') {\n // Check if any plan already exists in cart\n const existingPlan = prev.find((i) => i.type === 'PLAN');\n if (existingPlan) {\n // Replace existing plan with new one (can only have 1 plan)\n return prev.map((i) => (i.type === 'PLAN' ? { ...item, quantity: 1 } : i));\n }\n // Add new plan with quantity forced to 1\n return [...prev, { ...item, quantity: 1 }];\n }\n\n // For addons and credits, allow quantity increment\n const existing = prev.find((i) => i.id === item.id);\n if (existing) {\n return prev.map((i) =>\n i.id === item.id ? { ...i, quantity: i.quantity + item.quantity } : i\n );\n }\n return [...prev, item];\n });\n }, []);\n\n const removeItem = useCallback((itemId: string) => {\n setItems((prev) => prev.filter((i) => i.id !== itemId));\n }, []);\n\n const updateItemQuantity = useCallback(\n (itemId: string, quantity: number) => {\n if (quantity <= 0) {\n removeItem(itemId);\n return;\n }\n setItems((prev) =>\n prev.map((i) => {\n if (i.id !== itemId) return i;\n // Plans can only have quantity 1\n if (i.type === 'PLAN') {\n return { ...i, quantity: 1 };\n }\n return { ...i, quantity };\n })\n );\n },\n [removeItem]\n );\n\n const updateItemPrice = useCallback((itemId: string, price: number, currency?: Currency) => {\n setItems((prev) =>\n prev.map((i) => {\n if (i.id !== itemId) return i;\n // Update price and optionally currency\n const updates: Partial<CheckoutItem> = { price };\n if (currency) {\n updates.currency = currency;\n }\n return { ...i, ...updates };\n })\n );\n }, []);\n\n const clearItems = useCallback(() => {\n setItems([]);\n }, []);\n\n const handleSetBillingAccount = useCallback((account: CheckoutBillingAccount | null) => {\n setBillingAccount(account);\n // Reset credits usage when billing account changes\n setUseCredits(false);\n setCreditsToUse(0);\n }, []);\n\n const handleSetCurrency = useCallback(\n (currency: Currency) => {\n setSelectedCurrency(currency);\n // Skip conversion if items already have localized prices\n // (Plans/Addons with multi-currency pricing don't need conversion)\n const allItemsHaveLocalizedPrices = items.every(\n (item) => item.currency === currency || item.type === 'CREDITS'\n );\n\n // Only fetch conversion rate if items don't have localized prices\n if (total > 0 && currency !== 'USD' && !allItemsHaveLocalizedPrices) {\n convertCurrency({\n variables: {\n amount: total,\n toCurrency: currency,\n fromCurrency: 'USD',\n },\n });\n }\n },\n [total, convertCurrency, items]\n );\n\n const handleSetPaymentProvider = useCallback(\n (provider: PaymentProvider) => {\n setSelectedProvider(provider);\n // Razorpay only supports INR\n if (provider === 'RAZORPAY' && selectedCurrency !== 'INR') {\n setSelectedCurrency('INR');\n }\n },\n [selectedCurrency]\n );\n\n const validatePromoCode = useCallback(async (): Promise<PromoCodeResult> => {\n if (!promoCode.trim()) {\n return {\n valid: false,\n code: '',\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Please enter a promo code',\n };\n }\n\n try {\n setCheckoutState('loading');\n\n // Build validation input\n const validationInput = {\n code: promoCode.trim().toUpperCase(),\n userId: currentUser?.id ?? '',\n userEmail: currentUser?.email,\n amount: subtotal,\n currency: selectedCurrency,\n };\n\n const { data, error: queryError } = await validateCoupon({\n variables: {\n input: validationInput,\n },\n });\n\n if (queryError || !data?.validateCoupon) {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Failed to validate coupon',\n };\n }\n\n const response = data.validateCoupon;\n\n if (!response.valid) {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: response.error || 'Invalid coupon code',\n };\n }\n\n // Extract discount details from coupon\n const coupon = response.coupon;\n if (!coupon) {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Coupon data not found',\n };\n }\n\n // Use the ACTUAL discount amount from validation (includes caps and currency conversion)\n const actualDiscountAmount = response.discountAmount || 0;\n\n const discountType = coupon.type === 'FIXED_AMOUNT' ? 'FIXED' : 'PERCENTAGE';\n setPromoDiscount(actualDiscountAmount); // Use actual discount, not coupon.value\n setPromoDiscountType(discountType);\n setCheckoutState('idle');\n\n return {\n valid: true,\n code: coupon.code,\n discountType,\n discountValue: actualDiscountAmount, // Use actual discount, not coupon.value\n };\n } catch {\n setPromoDiscount(0);\n setCheckoutState('idle');\n return {\n valid: false,\n code: promoCode,\n discountType: 'PERCENTAGE',\n discountValue: 0,\n error: 'Failed to validate coupon',\n };\n }\n }, [currentUser?.email, currentUser?.id, promoCode, selectedCurrency, subtotal, validateCoupon]);\n\n const clearPromoCode = useCallback(() => {\n setPromoCode('');\n setPromoDiscount(0);\n setPromoDiscountType('PERCENTAGE');\n }, []);\n\n const handleSetCreditsToUse = useCallback(\n (amount: number) => {\n const maxCredits = Math.min(amount, availableCredits, subtotal - discountAmount);\n setCreditsToUse(Math.max(0, maxCredits));\n },\n [availableCredits, subtotal, discountAmount]\n );\n\n const initiatePayment = useCallback(async (): Promise<PaymentInitResponse> => {\n if (!billingAccount) {\n return {\n success: false,\n provider: 'AUTO',\n subtotal: 0,\n taxAmount: 0,\n taxRate: 0,\n total: 0,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: 'Please select a billing account',\n };\n }\n\n if (items.length === 0) {\n return {\n success: false,\n provider: 'AUTO',\n subtotal: 0,\n taxAmount: 0,\n taxRate: 0,\n total: 0,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: 'No items in checkout',\n };\n }\n\n try {\n setCheckoutState('processing');\n setError(null);\n\n // Determine purchase type from first item\n const firstItem = items[0];\n let purchaseType: PurchaseType = firstItem.type;\n\n // Check if this is a store order checkout (detect early)\n const storeOrderItem = items.find((item) => item.metadata?.storeOrderId);\n const storeOrderId = storeOrderItem?.metadata?.storeOrderId as string | undefined;\n let billingOrderId =\n (items.find((item) => item.metadata?.billingOrderId)?.metadata?.billingOrderId as\n string | undefined) || undefined;\n\n // For store orders, use 'ORDER' purchase type\n if (storeOrderId) {\n purchaseType = 'ORDER' as PurchaseType;\n }\n\n // Calculate amount to charge\n const chargeAmount = subtotal - discountAmount;\n\n // Determine provider\n let provider = selectedProvider;\n if (provider === 'AUTO') {\n provider = selectedCurrency === 'INR' ? 'RAZORPAY' : 'STRIPE';\n }\n\n // If this is a store order without a billing order yet, create one now.\n // Pass callbackGraphQL so the billing service can call back to the store service\n // after payment completes to mark the order confirmed.\n if (purchaseType === 'ORDER' && !billingOrderId && storeOrderId && billingAccount) {\n const orderItems = items\n .filter((item) => item.type === 'ORDER')\n .map((item) => ({\n name: item.name,\n description: item.description ?? undefined,\n quantity: item.quantity,\n unitPrice: item.price,\n metadata: {\n productId: item.metadata?.productId ?? undefined,\n variantId: item.metadata?.variantId ?? undefined,\n },\n }));\n\n const { data: billingOrderResult } = await createBillingOrderMutation({\n variables: {\n input: {\n // billingAccountId intentionally not set here — it gets\n // linked at initiatePayment time so the user can switch\n // billing accounts before confirming payment.\n externalOrderId: storeOrderId,\n externalService: 'STORE',\n currency: selectedCurrency,\n subtotal: chargeAmount,\n description: `Store order ${storeOrderId}`,\n items: orderItems,\n callbackGraphQL: {\n endpoint: 'http://global-int-gateway:4002/global/graphql',\n mutationName: 'updateStoreOrderPaymentStatus',\n },\n },\n },\n });\n\n if (billingOrderResult?.createBillingOrder?.success) {\n billingOrderId = billingOrderResult.createBillingOrder.order?.id;\n } else {\n const errMsg =\n billingOrderResult?.createBillingOrder?.error ?? 'Failed to create billing order';\n setCheckoutState('error');\n setError(errMsg);\n return {\n success: false,\n provider,\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: errMsg,\n };\n }\n }\n\n // ========================================\n // CREDITS PAYMENT - use spendCredits mutation\n // Supports multiple addons\n // ========================================\n if (provider === 'CREDITS') {\n const addonItems = items.filter((item) => item.type === 'ADDON');\n const planItem = items.find((item) => item.type === 'PLAN');\n\n const { data } = await spendCreditsMutation({\n variables: {\n input: {\n billingAccountId: billingAccount.id,\n planId: planItem ? (planItem.metadata?.planId as string) || planItem.id : undefined,\n // Multiple addons supported via addOnIds array\n addOnIds:\n addonItems.length > 0\n ? addonItems.map((item) => ({\n id: (item.metadata?.addonId as string) || item.id,\n quantity: item.quantity,\n }))\n : undefined,\n isAnnual: planItem?.metadata?.duration === 'yearly',\n // Include upgrade context if changing plans\n upgradeFromSubscriptionId: upgradeFromSubscriptionId || undefined,\n // Include downgrade context if downgrading\n downgradeFromSubscriptionId: downgradeFromSubscriptionId || undefined,\n // For addon purchases - subscription to attach to\n subscriptionId: addonItems.length > 0 ? selectedSubscriptionId : undefined,\n // For PER_SEAT plans - pass seat count for credit calculation + subscription storage\n seatCount: planItem?.metadata?.seatCount as number | undefined,\n },\n },\n });\n\n const response = data?.spendCredits;\n\n if (!response?.creditTransactionID) {\n setCheckoutState('error');\n setError('Failed to process credit payment');\n return {\n success: false,\n provider: 'CREDITS',\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: 'Failed to process credit payment',\n };\n }\n\n setCheckoutState('success');\n return {\n success: true,\n provider: 'CREDITS',\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n // Return transaction ID for success handling\n orderId: response.creditTransactionID,\n };\n }\n\n // ========================================\n // CARD PAYMENT (Stripe/Razorpay) - use initiatePayment\n // Supports single or multiple addons\n // Supports store orders (via orderId parameter)\n // ========================================\n const addonItems = items.filter((item) => item.type === 'ADDON');\n\n // Build addon IDs and quantities for the mutation\n const addonIds =\n addonItems.length > 0\n ? addonItems.map((item) => (item.metadata?.addonId as string) || item.id)\n : undefined;\n const quantities =\n addonItems.length > 0 ? addonItems.map((item) => item.quantity) : undefined;\n\n // Determine if this is an annual plan purchase\n const planItem = items.find((item) => item.type === 'PLAN');\n const isAnnualPlan = planItem?.metadata?.duration === 'yearly';\n\n // Determine auto-renew payment method\n const autoRenewPMId =\n purchaseType === 'PLAN' && autoRenew\n ? autoRenewPaymentMethodId || selectedPaymentMethodId || undefined\n : undefined;\n\n const { data } = await initiatePaymentMutation({\n variables: {\n input: {\n billingAccountId: billingAccount.id,\n amount: chargeAmount, // Note: Backend IGNORES this and calculates from DB\n currency: selectedCurrency,\n purchaseType,\n provider: provider,\n // Store order - use orderId parameter (billing order ID)\n orderId: billingOrderId || undefined,\n creditAmount: purchaseType === 'CREDITS' ? firstItem.quantity : undefined,\n planId:\n purchaseType === 'PLAN'\n ? (firstItem.metadata?.planId as string) || firstItem.id\n : undefined,\n // Single addon (backwards compatible) - skip if store order\n addonId:\n purchaseType === 'ADDON' && addonItems.length === 1 && !billingOrderId\n ? (addonItems[0].metadata?.addonId as string) || addonItems[0].id\n : undefined,\n quantity:\n purchaseType === 'ADDON' && addonItems.length === 1 && !billingOrderId\n ? addonItems[0].quantity\n : undefined,\n // Multiple addons (cart checkout) - skip if store order\n addonIds:\n purchaseType === 'ADDON' && addonItems.length > 1 && !billingOrderId\n ? addonIds\n : undefined,\n quantities:\n purchaseType === 'ADDON' && addonItems.length > 1 && !billingOrderId\n ? quantities\n : undefined,\n // For addon purchases - subscription to attach to (skip for store orders)\n subscriptionId:\n purchaseType === 'ADDON' && !billingOrderId ? selectedSubscriptionId : undefined,\n // Saved card support\n paymentMethodId: selectedPaymentMethodId || undefined,\n // Save card if: user checked save card OR auto-renewal is enabled (which requires a saved card)\n // Only save if not using existing card (selectedPaymentMethodId is null)\n saveCard: !selectedPaymentMethodId ? saveCard || autoRenew : undefined,\n // Include upgrade context if changing plans\n upgradeFromSubscriptionId: upgradeFromSubscriptionId || undefined,\n // Include downgrade context if downgrading\n downgradeFromSubscriptionId: downgradeFromSubscriptionId || undefined,\n // Annual plan flag (for pricing calculation)\n isAnnual: isAnnualPlan || undefined,\n // Promo code (validated and applied on backend)\n promoCode: promoCode || undefined,\n // Auto-renewal (for plan purchases only)\n autoRenew: purchaseType === 'PLAN' && autoRenew ? true : undefined,\n autoRenewPaymentMethodId: autoRenewPMId,\n },\n },\n });\n\n const response = data?.initiatePayment;\n\n if (!response?.success) {\n setCheckoutState('error');\n setError(response?.error ?? 'Payment initiation failed');\n return {\n success: false,\n provider: provider,\n subtotal: chargeAmount,\n taxAmount: 0,\n taxRate: 0,\n total: chargeAmount,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: response?.error ?? 'Payment initiation failed',\n };\n }\n\n // Note: Tax is already calculated upfront in the effect, don't overwrite\n // The response tax is in converted currency which would mess up display\n setCheckoutState('success');\n\n return response;\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'An error occurred';\n setCheckoutState('error');\n setError(errorMessage);\n return {\n success: false,\n provider: 'AUTO',\n subtotal: 0,\n taxAmount: 0,\n taxRate: 0,\n total: 0,\n taxType: 'NONE',\n jurisdiction: 'UNKNOWN',\n currency: selectedCurrency,\n error: errorMessage,\n };\n }\n }, [\n billingAccount,\n items,\n selectedCurrency,\n selectedProvider,\n subtotal,\n discountAmount,\n promoCode,\n selectedPaymentMethodId,\n saveCard,\n autoRenew,\n autoRenewPaymentMethodId,\n selectedSubscriptionId,\n upgradeFromSubscriptionId,\n downgradeFromSubscriptionId,\n initiatePaymentMutation,\n createBillingOrderMutation,\n spendCreditsMutation,\n ]);\n\n const reset = useCallback(() => {\n setItems([]);\n setBillingAccount(null);\n setSelectedProvider('AUTO');\n setSelectedCurrency(defaultCurrency);\n setPromoCode('');\n setPromoDiscount(0);\n setPromoDiscountType('PERCENTAGE');\n setUseCredits(false);\n setCreditsToUse(0);\n setCheckoutState('idle');\n setError(null);\n setTaxAmount(0);\n setTaxRate(0);\n setTaxType('NONE');\n setSelectedPaymentMethodId(null);\n setSaveCard(defaultSaveCard);\n setAutoRenew(false);\n setAutoRenewPaymentMethodId(null);\n setSelectedSubscriptionId(defaultSubscriptionId ?? null);\n }, [defaultCurrency, defaultSaveCard, defaultSubscriptionId]);\n\n return {\n // State\n items,\n billingAccount,\n selectedProvider,\n selectedCurrency,\n promoCode,\n promoDiscount,\n promoDiscountType,\n useCredits,\n creditsToUse,\n checkoutState,\n error,\n\n // Saved card state\n selectedPaymentMethodId,\n saveCard,\n\n // Auto-pay state\n autoRenew,\n autoRenewPaymentMethodId,\n\n // Subscription state (for addon purchases)\n selectedSubscriptionId,\n\n // Computed\n subtotal,\n subtotalUSD,\n discountAmount,\n taxAmount,\n taxRate,\n taxType,\n total,\n totalInSelectedCurrency,\n availableCredits,\n hasEnoughCredits,\n canPayWithCredits,\n isLoading,\n isProcessing,\n isCalculatingTax: false, // Tax is calculated synchronously now\n conversionRate,\n convertedAmount,\n\n // Actions\n addItem,\n removeItem,\n updateItemQuantity,\n updateItemPrice,\n clearItems,\n setBillingAccount: handleSetBillingAccount,\n setPaymentProvider: handleSetPaymentProvider,\n setCurrency: handleSetCurrency,\n setPromoCode,\n validatePromoCode,\n clearPromoCode,\n setUseCredits,\n setCreditsToUse: handleSetCreditsToUse,\n setSelectedPaymentMethodId,\n setSaveCard,\n setAutoRenew,\n setAutoRenewPaymentMethodId,\n setSelectedSubscriptionId,\n initiatePayment,\n reset,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/SubscriptionDetailPage.tsx"],"sourcesContent":["/**\n * Subscriptions Module - Subscription Detail Page\n * Displays detailed information about a single subscription\n */\n\nimport { type FC, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingEventEmitter } from '../../../hooks/useBillingEventEmitter';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useSubscription, useSubscriptionMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n useGetDefaultBillingAccountDashboardQuery,\n useGetPlanQuery,\n} from '../../../../generated/global-operations';\nimport { useWorkspaceQuotaOverview } from '../../usage/hooks/useUsage';\nimport {\n formatCurrency,\n formatDate,\n formatPlanDuration,\n formatSubscriptionStatus,\n formatPercentage,\n formatBytes,\n} from '../../../shared/utils/format';\nimport { getSubscriptionStatusColor, getQuotaProgressColor } from '../../../shared/utils/status';\nimport { Card, StatusPill, ProgressBar } from '../../../shared/ui';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n PagePurpose,\n NextSteps,\n type CTAAction,\n type NextStep,\n} from '@burdenoff/fe-libs/ui';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport type {\n SubscriptionStatus,\n QuotaAssignment,\n AddonSubscription,\n SubscriptionHistory,\n Transaction,\n} from '../../../shared/types';\nimport { PricingModel } from '../../../shared/types';\nimport { ManageSeatsModal } from '../components/ManageSeatsModal';\nimport {\n WorkspaceQuotaAssignmentModal,\n isWorkspaceScopedQuota,\n type WorkspaceQuotaToAssign,\n} from '../../checkout/components/WorkspaceQuotaAssignmentModal';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { WorkspaceQuotaOverviewItem } from '../../usage/hooks/useUsage';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatQuotaDisplayName(name: string, context?: Record<string, unknown> | null): string {\n if (context?.displayName && typeof context.displayName === 'string') return context.displayName;\n if (context?.label && typeof context.label === 'string') return context.label;\n const parts = name.split('.');\n const meaningful = parts.length > 1 ? parts.slice(1) : parts;\n return meaningful\n .map((p) => p.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()))\n .join(' ');\n}\n\nfunction formatResetPeriod(period: string): string {\n if (period === 'DAILY') return 'Resets daily';\n if (period === 'MONTHLY') return 'Resets monthly';\n return '';\n}\n\n// ============================================================================\n// Page Component\n// ============================================================================\n\nexport const SubscriptionDetailPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { subscriptionId } = useParams<{ subscriptionId: string }>();\n const navigateTo = useBillingNavigate();\n const { emit } = useBillingEventEmitter();\n const { orgId } = useBilling();\n const permissions = useBillingPermissions();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const { data: defaultBillingAccountData } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-first',\n });\n const billingAccountId =\n selectedAccountId ?? defaultBillingAccountData?.getDefaultBillingAccount?.id ?? undefined;\n const { subscription, isLoading, error, refetch } = useSubscription(\n subscriptionId,\n billingAccountId\n );\n const { cancelSubscription, isCanceling, updateSubscriptionSeats, isUpdatingSeats } =\n useSubscriptionMutations();\n\n const { workspaceId } = useBilling();\n\n // Workspace quota overview — used to detect if quotas are already active and show live usage\n const { overview: wsQuotaOverview, refetch: refetchWsOverview } = useWorkspaceQuotaOverview(\n workspaceId ?? undefined\n );\n\n // Plan features — the source of the workspace-scoped quotas included in this\n // plan. Subscription-level `quotas` only covers global-scope QuotaAssignment\n // rows; workspace-scoped quotas aren't provisioned as rows anywhere until\n // this subscription is activated for a workspace, so the \"Included quotas\"\n // preview in the assign modal has to be derived from the plan catalog.\n const { data: planFeaturesData } = useGetPlanQuery({\n variables: { id: subscription?.plan?.id ?? '' },\n skip: !subscription?.plan?.id,\n fetchPolicy: 'cache-first',\n });\n\n // Modal state\n const [showCancelModal, setShowCancelModal] = useState(false);\n const [showManageSeatsModal, setShowManageSeatsModal] = useState(false);\n const [showAssignWorkspaceModal, setShowAssignWorkspaceModal] = useState(false);\n const [cancelReason, setCancelReason] = useState('');\n const [cancelError, setCancelError] = useState<string | null>(null);\n\n // Handle cancel subscription\n const handleCancelSubscription = async () => {\n if (!subscription) return;\n\n setCancelError(null);\n try {\n await cancelSubscription({\n billingAccountId: billingAccountId ?? subscription.billingAccountId,\n subscriptionId: subscription.id,\n reason: cancelReason || 'User requested cancellation',\n });\n emit('billing.subscription.cancelled', {\n route: '/billing/subscriptions',\n entityId: subscription.id,\n source: 'subscription-detail',\n reason: cancelReason || 'User requested cancellation',\n });\n setShowCancelModal(false);\n setCancelReason('');\n await refetch();\n } catch (err) {\n setCancelError(err instanceof Error ? err.message : 'Failed to cancel subscription');\n }\n };\n\n // Permission check\n if (!permissions.canViewSubscriptions) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to view this subscription.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded-button\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 p-5 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n // Error state\n if (error || !subscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.notFound', 'Subscription not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message || 'The subscription you are looking for does not exist.'}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Back to Subscriptions\n </button>\n </div>\n </div>\n );\n }\n\n const plan = subscription.plan;\n const quotas = subscription.quotas || [];\n const addons = subscription.addonSubscriptions || [];\n const transactions = subscription.transactions || [];\n const history = subscription.history || [];\n const statusColor = getSubscriptionStatusColor(subscription.status);\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n\n // Workspace quota assignments that belong to this subscription\n const wsAssignmentsForSub = wsQuotaOverview.filter(\n (item) => item.assignment.subscriptionId === subscription.id && item.assignment.isActive\n );\n // `assignedWorkspaceId` is legacy and last-write-wins across every workspace\n // this subscription has ever been activated for — with multiple workspaces\n // per account, it stays truthy forever after the first activation and\n // permanently hides the \"Activate for Workspace\" action on every OTHER\n // workspace. `activatedWorkspaceIds` is the authoritative, multi-workspace-\n // aware set (backed by SubscriptionWorkspace); fall back to the workspace\n // assignments already loaded for this workspace in case the field isn't\n // populated yet.\n const isAssignedToWorkspace = workspaceId\n ? (subscription.activatedWorkspaceIds?.includes(workspaceId) ?? false) ||\n wsAssignmentsForSub.length > 0\n : false;\n\n // Workspace-scoped quotas included in this plan, for the \"Included quotas\"\n // preview in the assign modal (see planFeaturesData above).\n type PlanFeature = {\n quota?: { name?: string | null; limits?: Record<string, unknown> | null; quotaType?: string | null } | null;\n };\n const rawPlanFeatures: Array<PlanFeature | null | undefined> = planFeaturesData?.getPlan?.features ?? [];\n const planFeatures: PlanFeature[] = rawPlanFeatures.filter((f): f is PlanFeature => f != null);\n const workspaceQuotasToAssign: WorkspaceQuotaToAssign[] = planFeatures\n .filter((f: PlanFeature) => f.quota?.name && isWorkspaceScopedQuota(f.quota.name))\n .map((f: PlanFeature) => {\n const limits = f.quota?.limits;\n const limitsValue = limits && 'value' in limits ? Number(limits.value) : null;\n const limitsType = limits && 'type' in limits ? String(limits.type) : 'number';\n return {\n quotaName: f.quota!.name!,\n productId: plan?.productID ?? 'platform',\n limit: limitsType === 'number' ? limitsValue : null,\n noLimit: limitsType !== 'number',\n resetPeriod: 'NEVER',\n quotaMode: f.quota?.quotaType === 'POOLED' ? ('pooled' as const) : ('dedicated' as const),\n };\n });\n\n // Calculate days remaining\n const endDate = new Date(subscription.endDate);\n const startDate = new Date(subscription.startDate);\n const now = new Date();\n const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));\n const daysRemaining = Math.max(\n 0,\n Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))\n );\n const progressPercentage = Math.round(((totalDays - daysRemaining) / totalDays) * 100);\n\n // Recommended next actions for this subscription\n const nextSteps: NextStep[] = [\n ...(isActive && permissions.canUpdateSubscription\n ? [\n {\n id: 'change-plan',\n label: 'Review or change your plan',\n description: 'Upgrade, downgrade, or compare what each tier includes.',\n onClick: () =>\n navigateTo(\n withBillingAccountId(\n `/subscriptions/${subscription.id}/upgrade`,\n billingAccountId ?? subscription.billingAccountId\n )\n ),\n },\n ]\n : []),\n ...(isActive && workspaceId && !isAssignedToWorkspace\n ? [\n {\n id: 'activate-workspace',\n label: 'Activate quotas for your workspace',\n description: 'Turn on the workspace-scoped limits included in this plan.',\n onClick: () => setShowAssignWorkspaceModal(true),\n },\n ]\n : []),\n {\n id: 'track-usage',\n label: 'Track your usage',\n description: 'See how much of your included quota you have consumed.',\n onClick: () =>\n navigateTo(\n withBillingAccountId('/usage', billingAccountId ?? subscription.billingAccountId)\n ),\n },\n {\n id: 'view-invoices',\n label: 'Review invoices & payments',\n description: 'Check what has been billed for this subscription.',\n onClick: () =>\n navigateTo(\n withBillingAccountId('/invoices', billingAccountId ?? subscription.billingAccountId)\n ),\n },\n ];\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n aria-label=\"Back to subscriptions\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors duration-200\"\n >\n <svg className=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n <div>\n <div className=\"flex items-center gap-3 flex-wrap\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan?.name || 'Subscription'}\n </h1>\n <StatusPill status={statusColor} dot pulse={isActive}>\n {formatSubscriptionStatus(subscription.status)}\n </StatusPill>\n </div>\n <p className=\"text-sm text-text-secondary mt-1\">\n {plan?.pricingModel === PricingModel.PER_SEAT && plan.pricePerSeat\n ? `${formatCurrency(plan.pricePerSeat, plan.currency)} / seat / ${formatPlanDuration(plan.duration)} · ${subscription.seatCount ?? plan.minSeats ?? 1} seats = ${formatCurrency(plan.pricePerSeat * (subscription.seatCount ?? plan.minSeats ?? 1), plan.currency)} / ${formatPlanDuration(plan.duration)}`\n : plan\n ? `${formatCurrency(plan.price, plan.currency)} / ${formatPlanDuration(plan.duration)}`\n : 'Unknown plan'}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.detailPurpose',\n 'Everything about this subscription in one view — its billing period, the quotas and add-ons it includes, recent charges and its full status history. Use it to track what you are getting, activate workspace quotas, change your plan, or cancel.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <CTAOverflowMenu\n primary={\n isActive && permissions.canUpdateSubscription\n ? {\n label: 'Change Plan',\n onSelect: () =>\n navigateTo(\n withBillingAccountId(\n `/subscriptions/${subscription.id}/upgrade`,\n billingAccountId ?? subscription.billingAccountId\n )\n ),\n }\n : undefined\n }\n actions={\n [\n ...(isActive &&\n permissions.canUpdateSubscription &&\n plan?.pricingModel === PricingModel.PER_SEAT\n ? [\n {\n label: 'Manage Seats',\n onSelect: () => setShowManageSeatsModal(true),\n },\n ]\n : []),\n ...(isActive && permissions.canCancelSubscription && !subscription.canceledAt\n ? [\n {\n label: isCanceling ? 'Canceling…' : 'Cancel Subscription',\n onSelect: () => setShowCancelModal(true),\n disabled: isCanceling,\n intent: 'destructive' as const,\n },\n ]\n : []),\n ] satisfies CTAAction[]\n }\n />\n {subscription.canceledAt && (\n <span className=\"px-4 py-2 text-sm font-medium text-status-warning-text bg-status-warning-bg-subtle rounded-button\">\n Cancellation Scheduled\n </span>\n )}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Period Progress — primary status emphasis zone */}\n {isActive && (\n <EmphasisPanel className=\"p-5\">\n <div className=\"flex items-center justify-between mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Current Period</h2>\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {daysRemaining} days remaining\n </span>\n </div>\n <div className=\"space-y-2\">\n <ProgressBar\n value={progressPercentage}\n status=\"primary\"\n size=\"md\"\n aria-label=\"Billing period progress\"\n />\n <div className=\"flex items-center justify-between text-sm text-text-secondary\">\n <span>{formatDate(subscription.startDate, 'short')}</span>\n <span>{formatDate(subscription.endDate, 'short')}</span>\n </div>\n </div>\n </EmphasisPanel>\n )}\n\n {/* Plan Quotas */}\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Plan Quotas\n {quotas.length > 0 && (\n <span className=\"ml-2 text-sm font-normal text-text-muted tabular-nums\">\n ({quotas.length})\n </span>\n )}\n </h2>\n <p className=\"text-sm text-text-secondary mt-0.5\">Limits included in your plan</p>\n </div>\n <div className=\"p-5\">\n {quotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No quotas assigned to this subscription.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {quotas.map((quota) => (\n <QuotaUsageItem key={quota.id} quota={quota} />\n ))}\n </div>\n )}\n </div>\n </Card>\n\n {/* Workspace Quotas */}\n {isActive && workspaceId && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle flex items-center justify-between gap-4\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Workspace Quotas</h2>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n Active limits for your workspace\n </p>\n </div>\n {isAssignedToWorkspace ? (\n <StatusPill status=\"success\" dot>\n Active\n </StatusPill>\n ) : (\n <button\n type=\"button\"\n onClick={() => setShowAssignWorkspaceModal(true)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Activate for Workspace\n </button>\n )}\n </div>\n\n <div className=\"p-5\">\n {isAssignedToWorkspace ? (\n wsAssignmentsForSub.length > 0 ? (\n <div className=\"space-y-3\">\n {wsAssignmentsForSub.map((item) => (\n <WorkspaceQuotaItem key={item.assignment.id} item={item} />\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Quotas are activated on workspace{' '}\n <span className=\"font-mono text-xs text-text-primary bg-bg-sunken/60 rounded px-1.5 py-0.5\">\n {workspaceId}\n </span>\n . Usage data will appear here once quotas are consumed.\n </p>\n )\n ) : (\n <div className=\"flex items-start gap-3\">\n <div className=\"size-8 rounded-full bg-status-warning-bg-subtle flex items-center justify-center shrink-0 mt-0.5\">\n <svg\n className=\"size-4 text-status-warning-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n Workspace quotas not activated yet\n </p>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n Click{' '}\n <button\n type=\"button\"\n onClick={() => setShowAssignWorkspaceModal(true)}\n className=\"text-text-link hover:underline font-medium\"\n >\n Activate for Workspace\n </button>{' '}\n to enable the workspace-scoped limits included in this plan.\n </p>\n </div>\n </div>\n )}\n </div>\n\n {showAssignWorkspaceModal && (\n <WorkspaceQuotaAssignmentModal\n billingAccountId={billingAccountId ?? subscription.billingAccountId}\n subscriptionId={subscription.id}\n quotas={workspaceQuotasToAssign}\n workspaces={[{ id: workspaceId, name: 'Current workspace' }]}\n currentWorkspaceId={workspaceId}\n onAssigned={() => {\n setShowAssignWorkspaceModal(false);\n void refetch();\n void refetchWsOverview();\n }}\n onSkip={() => setShowAssignWorkspaceModal(false)}\n />\n )}\n </Card>\n )}\n\n {/* Addons */}\n {addons.length > 0 && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Addons{' '}\n <span className=\"text-sm font-normal text-text-muted tabular-nums\">\n ({addons.length})\n </span>\n </h2>\n </div>\n <div className=\"p-5\">\n <div className=\"space-y-3\">\n {addons.map((addonSub) => (\n <AddonItem key={addonSub.id} addonSubscription={addonSub} />\n ))}\n </div>\n </div>\n </Card>\n )}\n\n {/* Recent Transactions */}\n {transactions.length > 0 && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">Recent Transactions</h2>\n </div>\n <div className=\"divide-y divide-border-subtle\">\n {transactions.slice(0, 5).map((transaction) => (\n <TransactionItem key={transaction.id} transaction={transaction} />\n ))}\n </div>\n {transactions.length > 5 && (\n <div className=\"p-4 text-center border-t border-border-subtle\">\n <button type=\"button\" className=\"text-sm text-text-link hover:underline\">\n View all transactions\n </button>\n </div>\n )}\n </Card>\n )}\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Next steps guidance */}\n <NextSteps storageKey=\"billing-subscription-detail\" steps={nextSteps} />\n\n {/* Subscription Details */}\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">Details</h2>\n </div>\n <div className=\"p-5 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">Subscription ID</p>\n <p className=\"font-mono text-sm text-text-primary mt-0.5 break-all\">\n {subscription.id}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Plan</p>\n <p className=\"text-sm text-text-primary mt-0.5\">{plan?.name || 'Unknown'}</p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Billing Account</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {subscription.billingAccount?.name || 'Unknown'}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Start Date</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatDate(subscription.startDate)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">End Date</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatDate(subscription.endDate)}\n </p>\n </div>\n {subscription.nextBillingDate && (\n <div>\n <p className=\"text-sm text-text-secondary\">Next Billing</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatDate(subscription.nextBillingDate)}\n </p>\n </div>\n )}\n <div>\n <p className=\"text-sm text-text-secondary\">Auto Renewal</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {subscription.autoRenewal ? 'Enabled' : 'Disabled'}\n </p>\n </div>\n {subscription.paymentGateway && (\n <div>\n <p className=\"text-sm text-text-secondary\">Payment Gateway</p>\n <p className=\"text-sm text-text-primary mt-0.5 capitalize\">\n {subscription.paymentGateway}\n </p>\n </div>\n )}\n </div>\n </Card>\n\n {/* History */}\n {history.length > 0 && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">History</h2>\n </div>\n <div className=\"p-5\">\n <div className=\"space-y-3\">\n {history.slice(0, 5).map((item) => (\n <HistoryItem key={item.id} item={item} />\n ))}\n </div>\n </div>\n </Card>\n )}\n </div>\n </div>\n\n {/* Manage Seats Modal */}\n {showManageSeatsModal && subscription && (\n <ManageSeatsModal\n subscription={subscription}\n onClose={() => setShowManageSeatsModal(false)}\n onConfirm={async (seatCount) => {\n await updateSubscriptionSeats({ subscriptionId: subscription.id, seatCount });\n await refetch();\n }}\n isLoading={isUpdatingSeats}\n />\n )}\n\n {/* Cancel Subscription Modal */}\n {showCancelModal && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n <div\n className=\"absolute inset-0 bg-overlay-scrim\"\n onClick={() => setShowCancelModal(false)}\n />\n <div\n className=\"relative bg-bg-elevated border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] w-full max-w-md mx-4 p-6\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"cancel-subscription-title\"\n >\n <h3\n id=\"cancel-subscription-title\"\n className=\"text-lg font-semibold text-text-primary mb-2\"\n >\n Cancel Subscription\n </h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n Are you sure you want to cancel this subscription? Your subscription will remain\n active until{' '}\n <span className=\"font-medium text-text-primary\">\n {formatDate(subscription.endDate)}\n </span>\n , but will not renew automatically.\n </p>\n\n <div className=\"mb-4\">\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Reason for cancellation (optional)\n </label>\n <textarea\n value={cancelReason}\n onChange={(e) => setCancelReason(e.target.value)}\n placeholder=\"Please share why you're canceling...\"\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-transparent resize-none\"\n rows={3}\n />\n </div>\n\n {cancelError && (\n <div className=\"mb-4 p-3 bg-status-error-bg-subtle border border-status-error-border rounded-card\">\n <p className=\"text-sm text-status-error-text\">{cancelError}</p>\n </div>\n )}\n\n <div className=\"flex items-center justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => {\n setShowCancelModal(false);\n setCancelReason('');\n setCancelError(null);\n }}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors\"\n >\n Keep Subscription\n </button>\n <button\n type=\"button\"\n onClick={handleCancelSubscription}\n disabled={isCanceling}\n className=\"px-4 py-2 text-sm font-medium bg-action-danger-bg text-action-danger-text rounded-button hover:bg-action-danger-bgHover transition-colors duration-200 disabled:opacity-50\"\n >\n {isCanceling ? 'Canceling...' : 'Confirm Cancellation'}\n </button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Sub-components\n// ============================================================================\n\nconst QuotaUsageItem: FC<{ quota: QuotaAssignment }> = ({ quota }) => {\n const context = quota.context as\n | { displayName?: string; label?: string; description?: string }\n | null\n | undefined;\n const displayName = formatQuotaDisplayName(quota.name, context as Record<string, unknown> | null);\n const description =\n context?.description && typeof context.description === 'string' ? context.description : null;\n const limits = quota.limits as { value?: number } | null;\n const limit = limits?.value ?? 0;\n const used = quota.currentUsageSum ?? 0;\n const percentage = limit > 0 ? Math.min(100, (used / limit) * 100) : 0;\n const isBytes = quota.name.includes('bytes') || quota.name.includes('storage');\n const isPooled = quota.quotaType === 'POOLED';\n\n return (\n <div className=\"p-4 rounded-card border border-border-seam bg-bg-surface\">\n <div className=\"flex items-start justify-between gap-4 mb-2\">\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">{displayName}</p>\n {description && (\n <p className=\"text-xs text-text-secondary mt-0.5 line-clamp-2\">{description}</p>\n )}\n </div>\n <span className=\"text-sm font-medium text-text-primary shrink-0 tabular-nums\">\n {isBytes ? formatBytes(used) : used.toLocaleString()}\n {' / '}\n {limit > 0 ? (isBytes ? formatBytes(limit) : limit.toLocaleString()) : '∞'}\n </span>\n </div>\n\n {limit > 0 && (\n <div className=\"h-1.5 rounded-full bg-bg-sunken overflow-hidden mb-2\">\n <div\n className={`h-full rounded-full transition-all duration-300 ${getQuotaProgressColor(percentage)}`}\n style={{ width: `${percentage}%` }}\n />\n </div>\n )}\n\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span className=\"px-1.5 py-0.5 rounded bg-bg-sunken font-medium\">\n {isPooled ? 'Shared' : 'Per Workspace'}\n </span>\n <div className=\"flex items-center gap-3\">\n {quota.endtime && <span>Expires {formatDate(quota.endtime, 'short')}</span>}\n {limit > 0 && (\n <span\n className={\n percentage >= 90\n ? 'text-status-error-text font-medium'\n : percentage >= 70\n ? 'text-status-warning-text font-medium'\n : ''\n }\n >\n {formatPercentage(percentage)} used\n </span>\n )}\n </div>\n </div>\n </div>\n );\n};\n\nconst WorkspaceQuotaItem: FC<{ item: WorkspaceQuotaOverviewItem }> = ({ item }) => {\n const { assignment, effectiveUsed, usagePercentage } = item;\n const displayName = formatQuotaDisplayName(assignment.quotaName);\n const hasLimit = !assignment.noLimit && assignment.limit !== null;\n const limit = assignment.limit ?? 0;\n const isBytes =\n assignment.quotaName.includes('bytes') || assignment.quotaName.includes('storage');\n const resetLabel = formatResetPeriod(assignment.resetPeriod);\n\n return (\n <div className=\"p-4 rounded-card border border-border-seam bg-bg-surface\">\n <div className=\"flex items-start justify-between gap-4 mb-2\">\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">{displayName}</p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {assignment.quotaMode === 'pooled' ? 'Pooled across workspaces' : 'Dedicated'}\n {resetLabel && ` · ${resetLabel}`}\n </p>\n </div>\n <span className=\"text-sm font-medium text-text-primary shrink-0 tabular-nums\">\n {isBytes ? formatBytes(effectiveUsed) : effectiveUsed.toLocaleString()}\n {' / '}\n {assignment.noLimit\n ? '∞'\n : hasLimit\n ? isBytes\n ? formatBytes(limit)\n : limit.toLocaleString()\n : '∞'}\n </span>\n </div>\n\n {hasLimit && (\n <div className=\"h-1.5 rounded-full bg-bg-sunken overflow-hidden mb-2\">\n <div\n className={`h-full rounded-full transition-all duration-300 ${getQuotaProgressColor(usagePercentage)}`}\n style={{ width: `${Math.min(100, usagePercentage)}%` }}\n />\n </div>\n )}\n\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>Since {formatDate(assignment.createdAt, 'short')}</span>\n {hasLimit && (\n <span\n className={\n usagePercentage >= 90\n ? 'text-status-error-text font-medium'\n : usagePercentage >= 70\n ? 'text-status-warning-text font-medium'\n : ''\n }\n >\n {formatPercentage(usagePercentage)} used\n </span>\n )}\n </div>\n </div>\n );\n};\n\nconst AddonItem: FC<{ addonSubscription: AddonSubscription }> = ({ addonSubscription }) => {\n const addon = addonSubscription.addon;\n return (\n <div className=\"flex items-center justify-between p-3 rounded-card border border-border-seam bg-bg-sunken\">\n <div>\n <p className=\"font-medium text-text-primary\">{addon?.name || 'Unknown Addon'}</p>\n <p className=\"text-sm text-text-secondary\">Qty: {addonSubscription.quantity}</p>\n </div>\n {addon && (\n <p className=\"text-sm font-medium text-text-primary tabular-nums\">\n {formatCurrency(addon.price * addonSubscription.quantity, addon.currency)}\n </p>\n )}\n </div>\n );\n};\n\nconst TransactionItem: FC<{ transaction: Transaction }> = ({ transaction }) => {\n return (\n <div className=\"flex items-center justify-between px-5 py-4 hover:bg-bg-sunken transition-colors\">\n <div>\n <p className=\"font-medium text-text-primary capitalize\">{transaction.type}</p>\n <p className=\"text-sm text-text-secondary\">{formatDate(transaction.createdAt, 'short')}</p>\n </div>\n <div className=\"text-right\">\n <p\n className={`font-medium tabular-nums ${transaction.type === 'refund' ? 'text-status-error-text' : 'text-text-primary'}`}\n >\n {transaction.type === 'refund' ? '-' : ''}\n {formatCurrency(transaction.amount, transaction.currency)}\n </p>\n <p className=\"text-sm text-text-secondary capitalize\">{transaction.status}</p>\n </div>\n </div>\n );\n};\n\nconst HistoryItem: FC<{ item: SubscriptionHistory }> = ({ item }) => {\n return (\n <div className=\"flex items-start gap-3\">\n <div className=\"size-2 rounded-full bg-text-muted mt-2 shrink-0\" />\n <div>\n <p className=\"text-sm text-text-primary\">\n <span className=\"capitalize\">{item.fromStatus}</span>\n {' → '}\n <span className=\"capitalize font-medium\">{item.toStatus}</span>\n </p>\n {item.reason && <p className=\"text-xs text-text-secondary mt-0.5\">{item.reason}</p>}\n <p className=\"text-xs text-text-secondary mt-0.5\">{formatDate(item.createdAt, 'short')}</p>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SubscriptionDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/pages/SubscriptionDetailPage.tsx"],"sourcesContent":["/**\n * Subscriptions Module - Subscription Detail Page\n * Displays detailed information about a single subscription\n */\n\nimport { type FC, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingEventEmitter } from '../../../hooks/useBillingEventEmitter';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useSubscription, useSubscriptionMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n useGetDefaultBillingAccountDashboardQuery,\n useGetPlanQuery,\n} from '../../../../generated/global-operations';\nimport { useWorkspaceQuotaOverview } from '../../usage/hooks/useUsage';\nimport {\n formatCurrency,\n formatDate,\n formatPlanDuration,\n formatSubscriptionStatus,\n formatPercentage,\n formatBytes,\n} from '../../../shared/utils/format';\nimport { getSubscriptionStatusColor, getQuotaProgressColor } from '../../../shared/utils/status';\nimport { Card, StatusPill, ProgressBar } from '../../../shared/ui';\nimport {\n CTAOverflowMenu,\n EmphasisPanel,\n PagePurpose,\n NextSteps,\n type CTAAction,\n type NextStep,\n} from '@burdenoff/fe-libs/ui';\nimport { withBillingAccountId } from '../../../shared/utils/navigation';\nimport type {\n SubscriptionStatus,\n QuotaAssignment,\n AddonSubscription,\n SubscriptionHistory,\n Transaction,\n} from '../../../shared/types';\nimport { PricingModel } from '../../../shared/types';\nimport { ManageSeatsModal } from '../components/ManageSeatsModal';\nimport {\n WorkspaceQuotaAssignmentModal,\n isWorkspaceScopedQuota,\n type WorkspaceQuotaToAssign,\n} from '../../checkout/components/WorkspaceQuotaAssignmentModal';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { WorkspaceQuotaOverviewItem } from '../../usage/hooks/useUsage';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatQuotaDisplayName(name: string, context?: Record<string, unknown> | null): string {\n if (context?.displayName && typeof context.displayName === 'string') return context.displayName;\n if (context?.label && typeof context.label === 'string') return context.label;\n const parts = name.split('.');\n const meaningful = parts.length > 1 ? parts.slice(1) : parts;\n return meaningful\n .map((p) => p.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()))\n .join(' ');\n}\n\nfunction formatResetPeriod(period: string): string {\n if (period === 'DAILY') return 'Resets daily';\n if (period === 'MONTHLY') return 'Resets monthly';\n return '';\n}\n\n// ============================================================================\n// Page Component\n// ============================================================================\n\nexport const SubscriptionDetailPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { subscriptionId } = useParams<{ subscriptionId: string }>();\n const navigateTo = useBillingNavigate();\n const { emit } = useBillingEventEmitter();\n const { orgId } = useBilling();\n const permissions = useBillingPermissions();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const { data: defaultBillingAccountData } = useGetDefaultBillingAccountDashboardQuery({\n fetchPolicy: 'cache-first',\n });\n const billingAccountId =\n selectedAccountId ?? defaultBillingAccountData?.getDefaultBillingAccount?.id ?? undefined;\n const { subscription, isLoading, error, refetch } = useSubscription(\n subscriptionId,\n billingAccountId\n );\n const { cancelSubscription, isCanceling, updateSubscriptionSeats, isUpdatingSeats } =\n useSubscriptionMutations();\n\n const { workspaceId } = useBilling();\n\n // Workspace quota overview — used to detect if quotas are already active and show live usage\n const { overview: wsQuotaOverview, refetch: refetchWsOverview } = useWorkspaceQuotaOverview(\n workspaceId ?? undefined\n );\n\n // Plan features — the source of the workspace-scoped quotas included in this\n // plan. Subscription-level `quotas` only covers global-scope QuotaAssignment\n // rows; workspace-scoped quotas aren't provisioned as rows anywhere until\n // this subscription is activated for a workspace, so the \"Included quotas\"\n // preview in the assign modal has to be derived from the plan catalog.\n const { data: planFeaturesData } = useGetPlanQuery({\n variables: { id: subscription?.plan?.id ?? '' },\n skip: !subscription?.plan?.id,\n fetchPolicy: 'cache-first',\n });\n\n // Modal state\n const [showCancelModal, setShowCancelModal] = useState(false);\n const [showManageSeatsModal, setShowManageSeatsModal] = useState(false);\n const [showAssignWorkspaceModal, setShowAssignWorkspaceModal] = useState(false);\n const [cancelReason, setCancelReason] = useState('');\n const [cancelError, setCancelError] = useState<string | null>(null);\n\n // Handle cancel subscription\n const handleCancelSubscription = async () => {\n if (!subscription) return;\n\n setCancelError(null);\n try {\n await cancelSubscription({\n billingAccountId: billingAccountId ?? subscription.billingAccountId,\n subscriptionId: subscription.id,\n reason: cancelReason || 'User requested cancellation',\n });\n emit('billing.subscription.cancelled', {\n route: '/billing/subscriptions',\n entityId: subscription.id,\n source: 'subscription-detail',\n reason: cancelReason || 'User requested cancellation',\n });\n setShowCancelModal(false);\n setCancelReason('');\n await refetch();\n } catch (err) {\n setCancelError(err instanceof Error ? err.message : 'Failed to cancel subscription');\n }\n };\n\n // Permission check\n if (!permissions.canViewSubscriptions) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <div className=\"size-12 mx-auto rounded-full bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-secondary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary max-w-sm\">\n You don't have permission to view this subscription.\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded-button\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 p-5 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n // Error state\n if (error || !subscription) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <div className=\"size-12 mx-auto rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-6 text-status-error-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.notFound', 'Subscription not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message || 'The subscription you are looking for does not exist.'}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Back to Subscriptions\n </button>\n </div>\n </div>\n );\n }\n\n const plan = subscription.plan;\n const quotas = subscription.quotas || [];\n const addons = subscription.addonSubscriptions || [];\n const transactions = subscription.transactions || [];\n const history = subscription.history || [];\n const statusColor = getSubscriptionStatusColor(subscription.status);\n const isActive = subscription.status === ('active' as SubscriptionStatus);\n\n // Workspace quota assignments that belong to this subscription\n const wsAssignmentsForSub = wsQuotaOverview.filter(\n (item) => item.assignment.subscriptionId === subscription.id && item.assignment.isActive\n );\n // `assignedWorkspaceId` is legacy and last-write-wins across every workspace\n // this subscription has ever been activated for — with multiple workspaces\n // per account, it stays truthy forever after the first activation and\n // permanently hides the \"Activate for Workspace\" action on every OTHER\n // workspace. `activatedWorkspaceIds` is the authoritative, multi-workspace-\n // aware set (backed by SubscriptionWorkspace); fall back to the workspace\n // assignments already loaded for this workspace in case the field isn't\n // populated yet.\n const isAssignedToWorkspace = workspaceId\n ? (subscription.activatedWorkspaceIds?.includes(workspaceId) ?? false) ||\n wsAssignmentsForSub.length > 0\n : false;\n\n // Workspace-scoped quotas included in this plan, for the \"Included quotas\"\n // preview in the assign modal (see planFeaturesData above).\n type PlanFeature = {\n quota?: {\n name?: string | null;\n limits?: Record<string, unknown> | null;\n quotaType?: string | null;\n } | null;\n };\n const rawPlanFeatures: Array<PlanFeature | null | undefined> =\n planFeaturesData?.getPlan?.features ?? [];\n const planFeatures: PlanFeature[] = rawPlanFeatures.filter((f): f is PlanFeature => f != null);\n const workspaceQuotasToAssign: WorkspaceQuotaToAssign[] = planFeatures\n .filter((f: PlanFeature) => f.quota?.name && isWorkspaceScopedQuota(f.quota.name))\n .map((f: PlanFeature) => {\n const limits = f.quota?.limits;\n const limitsValue = limits && 'value' in limits ? Number(limits.value) : null;\n const limitsType = limits && 'type' in limits ? String(limits.type) : 'number';\n return {\n quotaName: f.quota!.name!,\n productId: plan?.productID ?? 'platform',\n limit: limitsType === 'number' ? limitsValue : null,\n noLimit: limitsType !== 'number',\n resetPeriod: 'NEVER',\n quotaMode: f.quota?.quotaType === 'POOLED' ? ('pooled' as const) : ('dedicated' as const),\n };\n });\n\n // Calculate days remaining\n const endDate = new Date(subscription.endDate);\n const startDate = new Date(subscription.startDate);\n const now = new Date();\n const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));\n const daysRemaining = Math.max(\n 0,\n Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))\n );\n const progressPercentage = Math.round(((totalDays - daysRemaining) / totalDays) * 100);\n\n // Recommended next actions for this subscription\n const nextSteps: NextStep[] = [\n ...(isActive && permissions.canUpdateSubscription\n ? [\n {\n id: 'change-plan',\n label: 'Review or change your plan',\n description: 'Upgrade, downgrade, or compare what each tier includes.',\n onClick: () =>\n navigateTo(\n withBillingAccountId(\n `/subscriptions/${subscription.id}/upgrade`,\n billingAccountId ?? subscription.billingAccountId\n )\n ),\n },\n ]\n : []),\n ...(isActive && workspaceId && !isAssignedToWorkspace\n ? [\n {\n id: 'activate-workspace',\n label: 'Activate quotas for your workspace',\n description: 'Turn on the workspace-scoped limits included in this plan.',\n onClick: () => setShowAssignWorkspaceModal(true),\n },\n ]\n : []),\n {\n id: 'track-usage',\n label: 'Track your usage',\n description: 'See how much of your included quota you have consumed.',\n onClick: () =>\n navigateTo(\n withBillingAccountId('/usage', billingAccountId ?? subscription.billingAccountId)\n ),\n },\n {\n id: 'view-invoices',\n label: 'Review invoices & payments',\n description: 'Check what has been billed for this subscription.',\n onClick: () =>\n navigateTo(\n withBillingAccountId('/invoices', billingAccountId ?? subscription.billingAccountId)\n ),\n },\n ];\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n aria-label=\"Back to subscriptions\"\n onClick={() => navigateTo(withBillingAccountId('/subscriptions', billingAccountId))}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors duration-200\"\n >\n <svg className=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n <div>\n <div className=\"flex items-center gap-3 flex-wrap\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan?.name || 'Subscription'}\n </h1>\n <StatusPill status={statusColor} dot pulse={isActive}>\n {formatSubscriptionStatus(subscription.status)}\n </StatusPill>\n </div>\n <p className=\"text-sm text-text-secondary mt-1\">\n {plan?.pricingModel === PricingModel.PER_SEAT && plan.pricePerSeat\n ? `${formatCurrency(plan.pricePerSeat, plan.currency)} / seat / ${formatPlanDuration(plan.duration)} · ${subscription.seatCount ?? plan.minSeats ?? 1} seats = ${formatCurrency(plan.pricePerSeat * (subscription.seatCount ?? plan.minSeats ?? 1), plan.currency)} / ${formatPlanDuration(plan.duration)}`\n : plan\n ? `${formatCurrency(plan.price, plan.currency)} / ${formatPlanDuration(plan.duration)}`\n : 'Unknown plan'}\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.subscriptions.detailPurpose',\n 'Everything about this subscription in one view — its billing period, the quotas and add-ons it includes, recent charges and its full status history. Use it to track what you are getting, activate workspace quotas, change your plan, or cancel.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <CTAOverflowMenu\n primary={\n isActive && permissions.canUpdateSubscription\n ? {\n label: 'Change Plan',\n onSelect: () =>\n navigateTo(\n withBillingAccountId(\n `/subscriptions/${subscription.id}/upgrade`,\n billingAccountId ?? subscription.billingAccountId\n )\n ),\n }\n : undefined\n }\n actions={\n [\n ...(isActive &&\n permissions.canUpdateSubscription &&\n plan?.pricingModel === PricingModel.PER_SEAT\n ? [\n {\n label: 'Manage Seats',\n onSelect: () => setShowManageSeatsModal(true),\n },\n ]\n : []),\n ...(isActive && permissions.canCancelSubscription && !subscription.canceledAt\n ? [\n {\n label: isCanceling ? 'Canceling…' : 'Cancel Subscription',\n onSelect: () => setShowCancelModal(true),\n disabled: isCanceling,\n intent: 'destructive' as const,\n },\n ]\n : []),\n ] satisfies CTAAction[]\n }\n />\n {subscription.canceledAt && (\n <span className=\"px-4 py-2 text-sm font-medium text-status-warning-text bg-status-warning-bg-subtle rounded-button\">\n Cancellation Scheduled\n </span>\n )}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Period Progress — primary status emphasis zone */}\n {isActive && (\n <EmphasisPanel className=\"p-5\">\n <div className=\"flex items-center justify-between mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Current Period</h2>\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {daysRemaining} days remaining\n </span>\n </div>\n <div className=\"space-y-2\">\n <ProgressBar\n value={progressPercentage}\n status=\"primary\"\n size=\"md\"\n aria-label=\"Billing period progress\"\n />\n <div className=\"flex items-center justify-between text-sm text-text-secondary\">\n <span>{formatDate(subscription.startDate, 'short')}</span>\n <span>{formatDate(subscription.endDate, 'short')}</span>\n </div>\n </div>\n </EmphasisPanel>\n )}\n\n {/* Plan Quotas */}\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Plan Quotas\n {quotas.length > 0 && (\n <span className=\"ml-2 text-sm font-normal text-text-muted tabular-nums\">\n ({quotas.length})\n </span>\n )}\n </h2>\n <p className=\"text-sm text-text-secondary mt-0.5\">Limits included in your plan</p>\n </div>\n <div className=\"p-5\">\n {quotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No quotas assigned to this subscription.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {quotas.map((quota) => (\n <QuotaUsageItem key={quota.id} quota={quota} />\n ))}\n </div>\n )}\n </div>\n </Card>\n\n {/* Workspace Quotas */}\n {isActive && workspaceId && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle flex items-center justify-between gap-4\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Workspace Quotas</h2>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n Active limits for your workspace\n </p>\n </div>\n {isAssignedToWorkspace ? (\n <StatusPill status=\"success\" dot>\n Active\n </StatusPill>\n ) : (\n <button\n type=\"button\"\n onClick={() => setShowAssignWorkspaceModal(true)}\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors duration-200\"\n >\n Activate for Workspace\n </button>\n )}\n </div>\n\n <div className=\"p-5\">\n {isAssignedToWorkspace ? (\n wsAssignmentsForSub.length > 0 ? (\n <div className=\"space-y-3\">\n {wsAssignmentsForSub.map((item) => (\n <WorkspaceQuotaItem key={item.assignment.id} item={item} />\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Quotas are activated on workspace{' '}\n <span className=\"font-mono text-xs text-text-primary bg-bg-sunken/60 rounded px-1.5 py-0.5\">\n {workspaceId}\n </span>\n . Usage data will appear here once quotas are consumed.\n </p>\n )\n ) : (\n <div className=\"flex items-start gap-3\">\n <div className=\"size-8 rounded-full bg-status-warning-bg-subtle flex items-center justify-center shrink-0 mt-0.5\">\n <svg\n className=\"size-4 text-status-warning-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n Workspace quotas not activated yet\n </p>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n Click{' '}\n <button\n type=\"button\"\n onClick={() => setShowAssignWorkspaceModal(true)}\n className=\"text-text-link hover:underline font-medium\"\n >\n Activate for Workspace\n </button>{' '}\n to enable the workspace-scoped limits included in this plan.\n </p>\n </div>\n </div>\n )}\n </div>\n\n {showAssignWorkspaceModal && (\n <WorkspaceQuotaAssignmentModal\n billingAccountId={billingAccountId ?? subscription.billingAccountId}\n subscriptionId={subscription.id}\n quotas={workspaceQuotasToAssign}\n workspaces={[{ id: workspaceId, name: 'Current workspace' }]}\n currentWorkspaceId={workspaceId}\n onAssigned={() => {\n setShowAssignWorkspaceModal(false);\n void refetch();\n void refetchWsOverview();\n }}\n onSkip={() => setShowAssignWorkspaceModal(false)}\n />\n )}\n </Card>\n )}\n\n {/* Addons */}\n {addons.length > 0 && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Addons{' '}\n <span className=\"text-sm font-normal text-text-muted tabular-nums\">\n ({addons.length})\n </span>\n </h2>\n </div>\n <div className=\"p-5\">\n <div className=\"space-y-3\">\n {addons.map((addonSub) => (\n <AddonItem key={addonSub.id} addonSubscription={addonSub} />\n ))}\n </div>\n </div>\n </Card>\n )}\n\n {/* Recent Transactions */}\n {transactions.length > 0 && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">Recent Transactions</h2>\n </div>\n <div className=\"divide-y divide-border-subtle\">\n {transactions.slice(0, 5).map((transaction) => (\n <TransactionItem key={transaction.id} transaction={transaction} />\n ))}\n </div>\n {transactions.length > 5 && (\n <div className=\"p-4 text-center border-t border-border-subtle\">\n <button type=\"button\" className=\"text-sm text-text-link hover:underline\">\n View all transactions\n </button>\n </div>\n )}\n </Card>\n )}\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Next steps guidance */}\n <NextSteps storageKey=\"billing-subscription-detail\" steps={nextSteps} />\n\n {/* Subscription Details */}\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">Details</h2>\n </div>\n <div className=\"p-5 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">Subscription ID</p>\n <p className=\"font-mono text-sm text-text-primary mt-0.5 break-all\">\n {subscription.id}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Plan</p>\n <p className=\"text-sm text-text-primary mt-0.5\">{plan?.name || 'Unknown'}</p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Billing Account</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {subscription.billingAccount?.name || 'Unknown'}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Start Date</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatDate(subscription.startDate)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">End Date</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatDate(subscription.endDate)}\n </p>\n </div>\n {subscription.nextBillingDate && (\n <div>\n <p className=\"text-sm text-text-secondary\">Next Billing</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatDate(subscription.nextBillingDate)}\n </p>\n </div>\n )}\n <div>\n <p className=\"text-sm text-text-secondary\">Auto Renewal</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {subscription.autoRenewal ? 'Enabled' : 'Disabled'}\n </p>\n </div>\n {subscription.paymentGateway && (\n <div>\n <p className=\"text-sm text-text-secondary\">Payment Gateway</p>\n <p className=\"text-sm text-text-primary mt-0.5 capitalize\">\n {subscription.paymentGateway}\n </p>\n </div>\n )}\n </div>\n </Card>\n\n {/* History */}\n {history.length > 0 && (\n <Card className=\"overflow-hidden\">\n <div className=\"p-5 border-b border-border-subtle\">\n <h2 className=\"text-base font-semibold text-text-primary\">History</h2>\n </div>\n <div className=\"p-5\">\n <div className=\"space-y-3\">\n {history.slice(0, 5).map((item) => (\n <HistoryItem key={item.id} item={item} />\n ))}\n </div>\n </div>\n </Card>\n )}\n </div>\n </div>\n\n {/* Manage Seats Modal */}\n {showManageSeatsModal && subscription && (\n <ManageSeatsModal\n subscription={subscription}\n onClose={() => setShowManageSeatsModal(false)}\n onConfirm={async (seatCount) => {\n await updateSubscriptionSeats({ subscriptionId: subscription.id, seatCount });\n await refetch();\n }}\n isLoading={isUpdatingSeats}\n />\n )}\n\n {/* Cancel Subscription Modal */}\n {showCancelModal && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n <div\n className=\"absolute inset-0 bg-overlay-scrim\"\n onClick={() => setShowCancelModal(false)}\n />\n <div\n className=\"relative bg-bg-elevated border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] w-full max-w-md mx-4 p-6\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"cancel-subscription-title\"\n >\n <h3\n id=\"cancel-subscription-title\"\n className=\"text-lg font-semibold text-text-primary mb-2\"\n >\n Cancel Subscription\n </h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n Are you sure you want to cancel this subscription? Your subscription will remain\n active until{' '}\n <span className=\"font-medium text-text-primary\">\n {formatDate(subscription.endDate)}\n </span>\n , but will not renew automatically.\n </p>\n\n <div className=\"mb-4\">\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Reason for cancellation (optional)\n </label>\n <textarea\n value={cancelReason}\n onChange={(e) => setCancelReason(e.target.value)}\n placeholder=\"Please share why you're canceling...\"\n className=\"w-full px-3 py-2 text-sm border border-border-subtle rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-transparent resize-none\"\n rows={3}\n />\n </div>\n\n {cancelError && (\n <div className=\"mb-4 p-3 bg-status-error-bg-subtle border border-status-error-border rounded-card\">\n <p className=\"text-sm text-status-error-text\">{cancelError}</p>\n </div>\n )}\n\n <div className=\"flex items-center justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => {\n setShowCancelModal(false);\n setCancelReason('');\n setCancelError(null);\n }}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors\"\n >\n Keep Subscription\n </button>\n <button\n type=\"button\"\n onClick={handleCancelSubscription}\n disabled={isCanceling}\n className=\"px-4 py-2 text-sm font-medium bg-action-danger-bg text-action-danger-text rounded-button hover:bg-action-danger-bgHover transition-colors duration-200 disabled:opacity-50\"\n >\n {isCanceling ? 'Canceling...' : 'Confirm Cancellation'}\n </button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Sub-components\n// ============================================================================\n\nconst QuotaUsageItem: FC<{ quota: QuotaAssignment }> = ({ quota }) => {\n const context = quota.context as\n { displayName?: string; label?: string; description?: string } | null | undefined;\n const displayName = formatQuotaDisplayName(quota.name, context as Record<string, unknown> | null);\n const description =\n context?.description && typeof context.description === 'string' ? context.description : null;\n const limits = quota.limits as { value?: number } | null;\n const limit = limits?.value ?? 0;\n const used = quota.currentUsageSum ?? 0;\n const percentage = limit > 0 ? Math.min(100, (used / limit) * 100) : 0;\n const isBytes = quota.name.includes('bytes') || quota.name.includes('storage');\n const isPooled = quota.quotaType === 'POOLED';\n\n return (\n <div className=\"p-4 rounded-card border border-border-seam bg-bg-surface\">\n <div className=\"flex items-start justify-between gap-4 mb-2\">\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">{displayName}</p>\n {description && (\n <p className=\"text-xs text-text-secondary mt-0.5 line-clamp-2\">{description}</p>\n )}\n </div>\n <span className=\"text-sm font-medium text-text-primary shrink-0 tabular-nums\">\n {isBytes ? formatBytes(used) : used.toLocaleString()}\n {' / '}\n {limit > 0 ? (isBytes ? formatBytes(limit) : limit.toLocaleString()) : '∞'}\n </span>\n </div>\n\n {limit > 0 && (\n <div className=\"h-1.5 rounded-full bg-bg-sunken overflow-hidden mb-2\">\n <div\n className={`h-full rounded-full transition-all duration-300 ${getQuotaProgressColor(percentage)}`}\n style={{ width: `${percentage}%` }}\n />\n </div>\n )}\n\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span className=\"px-1.5 py-0.5 rounded bg-bg-sunken font-medium\">\n {isPooled ? 'Shared' : 'Per Workspace'}\n </span>\n <div className=\"flex items-center gap-3\">\n {quota.endtime && <span>Expires {formatDate(quota.endtime, 'short')}</span>}\n {limit > 0 && (\n <span\n className={\n percentage >= 90\n ? 'text-status-error-text font-medium'\n : percentage >= 70\n ? 'text-status-warning-text font-medium'\n : ''\n }\n >\n {formatPercentage(percentage)} used\n </span>\n )}\n </div>\n </div>\n </div>\n );\n};\n\nconst WorkspaceQuotaItem: FC<{ item: WorkspaceQuotaOverviewItem }> = ({ item }) => {\n const { assignment, effectiveUsed, usagePercentage } = item;\n const displayName = formatQuotaDisplayName(assignment.quotaName);\n const hasLimit = !assignment.noLimit && assignment.limit !== null;\n const limit = assignment.limit ?? 0;\n const isBytes =\n assignment.quotaName.includes('bytes') || assignment.quotaName.includes('storage');\n const resetLabel = formatResetPeriod(assignment.resetPeriod);\n\n return (\n <div className=\"p-4 rounded-card border border-border-seam bg-bg-surface\">\n <div className=\"flex items-start justify-between gap-4 mb-2\">\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">{displayName}</p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {assignment.quotaMode === 'pooled' ? 'Pooled across workspaces' : 'Dedicated'}\n {resetLabel && ` · ${resetLabel}`}\n </p>\n </div>\n <span className=\"text-sm font-medium text-text-primary shrink-0 tabular-nums\">\n {isBytes ? formatBytes(effectiveUsed) : effectiveUsed.toLocaleString()}\n {' / '}\n {assignment.noLimit\n ? '∞'\n : hasLimit\n ? isBytes\n ? formatBytes(limit)\n : limit.toLocaleString()\n : '∞'}\n </span>\n </div>\n\n {hasLimit && (\n <div className=\"h-1.5 rounded-full bg-bg-sunken overflow-hidden mb-2\">\n <div\n className={`h-full rounded-full transition-all duration-300 ${getQuotaProgressColor(usagePercentage)}`}\n style={{ width: `${Math.min(100, usagePercentage)}%` }}\n />\n </div>\n )}\n\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>Since {formatDate(assignment.createdAt, 'short')}</span>\n {hasLimit && (\n <span\n className={\n usagePercentage >= 90\n ? 'text-status-error-text font-medium'\n : usagePercentage >= 70\n ? 'text-status-warning-text font-medium'\n : ''\n }\n >\n {formatPercentage(usagePercentage)} used\n </span>\n )}\n </div>\n </div>\n );\n};\n\nconst AddonItem: FC<{ addonSubscription: AddonSubscription }> = ({ addonSubscription }) => {\n const addon = addonSubscription.addon;\n return (\n <div className=\"flex items-center justify-between p-3 rounded-card border border-border-seam bg-bg-sunken\">\n <div>\n <p className=\"font-medium text-text-primary\">{addon?.name || 'Unknown Addon'}</p>\n <p className=\"text-sm text-text-secondary\">Qty: {addonSubscription.quantity}</p>\n </div>\n {addon && (\n <p className=\"text-sm font-medium text-text-primary tabular-nums\">\n {formatCurrency(addon.price * addonSubscription.quantity, addon.currency)}\n </p>\n )}\n </div>\n );\n};\n\nconst TransactionItem: FC<{ transaction: Transaction }> = ({ transaction }) => {\n return (\n <div className=\"flex items-center justify-between px-5 py-4 hover:bg-bg-sunken transition-colors\">\n <div>\n <p className=\"font-medium text-text-primary capitalize\">{transaction.type}</p>\n <p className=\"text-sm text-text-secondary\">{formatDate(transaction.createdAt, 'short')}</p>\n </div>\n <div className=\"text-right\">\n <p\n className={`font-medium tabular-nums ${transaction.type === 'refund' ? 'text-status-error-text' : 'text-text-primary'}`}\n >\n {transaction.type === 'refund' ? '-' : ''}\n {formatCurrency(transaction.amount, transaction.currency)}\n </p>\n <p className=\"text-sm text-text-secondary capitalize\">{transaction.status}</p>\n </div>\n </div>\n );\n};\n\nconst HistoryItem: FC<{ item: SubscriptionHistory }> = ({ item }) => {\n return (\n <div className=\"flex items-start gap-3\">\n <div className=\"size-2 rounded-full bg-text-muted mt-2 shrink-0\" />\n <div>\n <p className=\"text-sm text-text-primary\">\n <span className=\"capitalize\">{item.fromStatus}</span>\n {' → '}\n <span className=\"capitalize font-medium\">{item.toStatus}</span>\n </p>\n {item.reason && <p className=\"text-xs text-text-secondary mt-0.5\">{item.reason}</p>}\n <p className=\"text-xs text-text-secondary mt-0.5\">{formatDate(item.createdAt, 'short')}</p>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusPill.js","names":[],"sources":["../../../../src/billing/shared/ui/StatusPill.tsx"],"sourcesContent":["/**\n * Boff UI — StatusPill primitive\n *\n * Subtle status badge following the Boff UI recipe\n * (
|
|
1
|
+
{"version":3,"file":"StatusPill.js","names":[],"sources":["../../../../src/billing/shared/ui/StatusPill.tsx"],"sourcesContent":["/**\n * Boff UI — StatusPill primitive\n *\n * Subtle status badge following the Boff UI recipe\n * (for example, `bg-status-success-bg-subtle text-status-success-text`;\n * neutral uses `bg-bg-sunken text-text-secondary`). Always keeps the text\n * label and an optional dot — never color-only. Maps the billing module's\n * existing `SemanticStatus` palette to the canonical tokens.\n *\n * Visual-only primitive.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\nimport type { SemanticStatus } from '../utils/tokens';\n\nconst PILL_TONE: Record<SemanticStatus, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-status-info-bg-subtle text-status-info-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n primary: 'bg-[var(--color-accent-soft)] text-text-link',\n};\n\nconst DOT_TONE: Record<SemanticStatus, string> = {\n success: 'bg-status-success-bg',\n warning: 'bg-status-warning-bg',\n error: 'bg-status-error-bg',\n info: 'bg-status-info-bg',\n neutral: 'bg-text-muted',\n primary: 'bg-action-primary-bg',\n};\n\ninterface StatusPillProps {\n status: SemanticStatus;\n children: ReactNode;\n /** Show a leading status dot. */\n dot?: boolean;\n /** Pulse the dot (e.g. live / offline indicators). */\n pulse?: boolean;\n className?: string;\n}\n\nexport function StatusPill({\n status,\n children,\n dot = false,\n pulse = false,\n className,\n}: StatusPillProps) {\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n PILL_TONE[status],\n className\n )}\n >\n {dot && (\n <span\n className={cn(\n 'size-1.5 rounded-full',\n DOT_TONE[status],\n pulse && 'motion-safe:animate-vc-glow-pulse'\n )}\n />\n )}\n {children}\n </span>\n );\n}\n"],"mappings":";;;AAgBA,IAAM,IAA4C;CAChD,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACT,SAAS;AACX,GAEM,IAA2C;CAC/C,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACT,SAAS;AACX;AAYA,SAAgB,EAAW,EACzB,WACA,aACA,SAAM,IACN,WAAQ,IACR,gBACkB;CAClB,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAU,IACV,CACF;YALF,CAOG,KACC,kBAAC,QAAD,EACE,WAAW,EACT,yBACA,EAAS,IACT,KAAS,mCACX,EACD,CAAA,GAEF,CACG;;AAEV"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-billing",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.828.2",
|
|
4
4
|
"description": "Billing microfrontend for Burdenoff products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"dev": "vite",
|
|
25
|
-
"build": "tsc && bash scripts/build-limits.sh --gate vite build && bun
|
|
25
|
+
"build": "tsc && bash scripts/build-limits.sh --gate vite build && bun run dist-tailwind-guard",
|
|
26
|
+
"dist-tailwind-guard": "bun scripts/dist-tailwind-guard.ts",
|
|
26
27
|
"schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
|
|
27
28
|
"codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
|
|
28
29
|
"codegen:all": "bun run schema:pull && bun run codegen",
|
|
@@ -87,7 +88,7 @@
|
|
|
87
88
|
"@apollo/client": "^4.0.9",
|
|
88
89
|
"react": "^19.0.0",
|
|
89
90
|
"react-dom": "^19.0.0",
|
|
90
|
-
"zustand": "^5.0.
|
|
91
|
+
"zustand": "^5.0.3"
|
|
91
92
|
},
|
|
92
93
|
"publishConfig": {
|
|
93
94
|
"access": "public"
|