@burdenoff/microfe-billing 2026.802.1 → 2026.805.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"PlanDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Detail Page\n * Displays detailed information about a single plan\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan, usePlanMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n} from '../../../shared/utils/format';\nimport { getPlanFeatureQuantity } from '../../../shared/utils/planFeatureQuantity';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\n\n/**\n * Combined quota info for display\n */\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n count: number;\n}\n\n/**\n * Combines duplicate quotas by quota ID and aggregates their values\n */\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n\n for (const feature of features) {\n if (!feature.quota) continue;\n\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n existing.count += 1;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n count: 1,\n });\n }\n }\n\n return Array.from(quotaMap.values());\n}\n\nexport const PlanDetailPage: 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 { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n const { togglePlan, isToggling } = usePlanMutations();\n\n // Combine duplicate quotas for cleaner display (must be before any returns)\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n // Permission check\n if (!permissions.canViewPlans) {\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-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-muted\"\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\">\n {tr('billing.accessDenied.title', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-muted max-w-sm\">\n {tr(\n 'billing.plans.noViewPlanPermission',\n \"You don't have permission to view this plan.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\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-subtle rounded-lg p-6 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 className=\"h-4 w-3/4 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div 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-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n );\n }\n\n // Error state\n if (error || !plan) {\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.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-muted\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\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\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n const handleToggle = async () => {\n if (!permissions.canManagePlans) return;\n try {\n await togglePlan(plan.id, !plan.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle plan:', err);\n }\n };\n\n return (\n <div className=\"space-y-6 p-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 onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-muted hover:text-text-primary hover:bg-bg-sunken rounded-md transition-colors\"\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\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <span\n className={`px-2 py-1 text-xs font-medium rounded ${\n plan.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {plan.isActive ? 'Active' : 'Inactive'}\n </span>\n </div>\n <p className=\"text-sm text-text-muted mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.detailPurpose',\n 'A full breakdown of this plan — its price, billing period and every quota and feature it grants. Use it to decide whether the plan fits before subscribing, or (as an admin) to edit or activate it.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {permissions.canCreateSubscription && plan.isActive && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\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\"\n >\n Subscribe to Plan\n </button>\n )}\n {permissions.canManagePlans && (\n <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/plans/${plan.id}/edit`)}\n className=\"px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Edit Plan\n </button>\n <button\n type=\"button\"\n onClick={handleToggle}\n disabled={isToggling}\n className={`px-4 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n plan.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {isToggling\n ? tr('billing.plans.processing', 'Processing...')\n : plan.isActive\n ? tr('billing.plans.deactivate', 'Deactivate')\n : tr('billing.plans.activate', 'Activate')}\n </button>\n </>\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 {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-4\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-muted\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n <p className=\"text-sm text-text-muted mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-6 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken/30\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-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=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-muted capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Plan Details Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-6 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.planDetails', 'Plan Details')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.planName', 'Plan Name')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">{plan.name}</p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">{tr('billing.plans.status', 'Status')}</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n <span\n className={`inline-flex items-center px-2 py-0.5 text-xs font-medium rounded ${\n plan.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {plan.isActive ? 'Active' : 'Inactive'}\n </span>\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.currency', 'Currency')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">{plan.currency}</p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n </div>\n </div>\n\n {/* Quick Stats Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-6 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.statistics', 'Statistics')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.uniqueQuotas', 'Unique Quotas')}\n </p>\n <p className=\"font-semibold text-text-primary\">{combinedQuotas.length}</p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"PlanDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Detail Page\n * Displays detailed information about a single plan\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan, usePlanMutations } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n formatQuotaValue,\n} from '../../../shared/utils/format';\nimport {\n getPlanFeatureQuantity,\n getPlanFeatureUnit,\n getQuotaDisplayName,\n} from '../../../shared/utils/planFeatureQuantity';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\n\n/**\n * Combined quota info for display\n */\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n count: number;\n unit?: string;\n}\n\n/**\n * Combines duplicate quotas by quota ID and aggregates their values\n */\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n\n for (const feature of features) {\n if (!feature.quota) continue;\n\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n existing.count += 1;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: getQuotaDisplayName(feature),\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n count: 1,\n unit: getPlanFeatureUnit(feature),\n });\n }\n }\n\n return Array.from(quotaMap.values());\n}\n\nexport const PlanDetailPage: 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 { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n const { togglePlan, isToggling } = usePlanMutations();\n\n // Combine duplicate quotas for cleaner display (must be before any returns)\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n // Permission check\n if (!permissions.canViewPlans) {\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-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-6 text-text-muted\"\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\">\n {tr('billing.accessDenied.title', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-muted max-w-sm\">\n {tr(\n 'billing.plans.noViewPlanPermission',\n \"You don't have permission to view this plan.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n // Loading state\n if (isLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\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-subtle rounded-lg p-6 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 className=\"h-4 w-3/4 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div 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-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n );\n }\n\n // Error state\n if (error || !plan) {\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.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-muted\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\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\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n const handleToggle = async () => {\n if (!permissions.canManagePlans) return;\n try {\n await togglePlan(plan.id, !plan.isActive);\n await refetch();\n } catch (err) {\n console.error('Failed to toggle plan:', err);\n }\n };\n\n return (\n <div className=\"space-y-6 p-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 onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-muted hover:text-text-primary hover:bg-bg-sunken rounded-md transition-colors\"\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\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <span\n className={`px-2 py-1 text-xs font-medium rounded ${\n plan.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {plan.isActive ? 'Active' : 'Inactive'}\n </span>\n </div>\n <p className=\"text-sm text-text-muted mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.detailPurpose',\n 'A full breakdown of this plan — its price, billing period and every quota and feature it grants. Use it to decide whether the plan fits before subscribing, or (as an admin) to edit or activate it.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {permissions.canCreateSubscription && plan.isActive && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\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\"\n >\n Subscribe to Plan\n </button>\n )}\n {permissions.canManagePlans && (\n <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/plans/${plan.id}/edit`)}\n className=\"px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Edit Plan\n </button>\n <button\n type=\"button\"\n onClick={handleToggle}\n disabled={isToggling}\n className={`px-4 py-2 text-sm font-medium border border-border-subtle rounded-md transition-colors ${\n plan.isActive\n ? 'text-status-error-text hover:bg-status-error-bg-subtle'\n : 'text-status-success-text hover:bg-status-success-bg-subtle'\n }`}\n >\n {isToggling\n ? tr('billing.plans.processing', 'Processing...')\n : plan.isActive\n ? tr('billing.plans.deactivate', 'Deactivate')\n : tr('billing.plans.activate', 'Activate')}\n </button>\n </>\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 {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-4\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-muted\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n <p className=\"text-sm text-text-muted mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-6 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken/30\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-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=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {formatQuotaValue(quota.totalValue, quota.unit)}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-muted capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-muted rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Plan Details Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-6 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.planDetails', 'Plan Details')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.planName', 'Plan Name')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">{plan.name}</p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">{tr('billing.plans.status', 'Status')}</p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n <span\n className={`inline-flex items-center px-2 py-0.5 text-xs font-medium rounded ${\n plan.isActive\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {plan.isActive ? 'Active' : 'Inactive'}\n </span>\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.currency', 'Currency')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">{plan.currency}</p>\n </div>\n <div>\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n </div>\n </div>\n\n {/* Quick Stats Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-6 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.statistics', 'Statistics')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-text-muted\">\n {tr('billing.plans.uniqueQuotas', 'Unique Quotas')}\n </p>\n <p className=\"font-semibold text-text-primary\">{combinedQuotas.length}</p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1,16 +1,16 @@
1
1
  import { CreditTransactionType as e, InvoiceStatus as t, PlanDuration as n, SubscriptionStatus as r } from "../types/graphql.js";
2
- import { formatBytes as i, formatCurrency as a, formatDate as o, formatDateTime as s, formatNumber as c, formatPercentage as l, formatRelativeTime as u } from "@burdenoff/fe-libs/shared/utils";
2
+ import { formatBytes as i, formatBytes as a, formatCurrency as o, formatDate as s, formatDateTime as c, formatNumber as l, formatPercentage as u, formatRelativeTime as d } from "@burdenoff/fe-libs/shared/utils";
3
3
  //#region src/billing/shared/utils/format.ts
4
- function d(e, t = "USD", n = "en-US") {
5
- return t.toUpperCase() === "CREDITS" ? f(e) : a(e, t, n);
4
+ function f(e, t = "USD", n = "en-US") {
5
+ return t.toUpperCase() === "CREDITS" ? p(e) : o(e, t, n);
6
6
  }
7
- function f(e) {
7
+ function p(e) {
8
8
  return Number.isInteger(e) ? `${e.toLocaleString()} Credits` : `${e.toLocaleString(void 0, {
9
9
  minimumFractionDigits: 0,
10
10
  maximumFractionDigits: 2
11
11
  })} Credits`;
12
12
  }
13
- function p(e) {
13
+ function m(e) {
14
14
  return {
15
15
  [r.ACTIVE]: "Active",
16
16
  [r.CANCELED]: "Canceled",
@@ -18,7 +18,7 @@ function p(e) {
18
18
  [r.UPGRADED]: "Upgraded"
19
19
  }[e] || e;
20
20
  }
21
- function m(e) {
21
+ function h(e) {
22
22
  return {
23
23
  [t.DRAFT]: "Draft",
24
24
  [t.OPEN]: "Open",
@@ -27,19 +27,19 @@ function m(e) {
27
27
  [t.UNCOLLECTIBLE]: "Uncollectible"
28
28
  }[e] || e;
29
29
  }
30
- function h(e) {
30
+ function g(e) {
31
31
  return {
32
32
  [n.MONTHLY]: "month",
33
33
  [n.YEARLY]: "year"
34
34
  }[e] || e;
35
35
  }
36
- function g(e) {
36
+ function _(e) {
37
37
  return {
38
38
  [n.MONTHLY]: "Monthly",
39
39
  [n.YEARLY]: "Yearly"
40
40
  }[e] || e;
41
41
  }
42
- function _(t) {
42
+ function v(t) {
43
43
  return {
44
44
  [e.PURCHASE]: "Purchase",
45
45
  [e.USAGE]: "Usage",
@@ -47,10 +47,13 @@ function _(t) {
47
47
  [e.GRANTED]: "Granted"
48
48
  }[t] || t;
49
49
  }
50
- function v(e) {
50
+ function y(e) {
51
51
  return `#${e}`;
52
52
  }
53
+ function b(e, t) {
54
+ return t === "bytes" ? i(e) : e.toLocaleString();
55
+ }
53
56
  //#endregion
54
- export { i as formatBytes, _ as formatCreditTransactionType, f as formatCredits, d as formatCurrency, o as formatDate, s as formatDateTime, v as formatInvoiceNumber, m as formatInvoiceStatus, c as formatNumber, l as formatPercentage, h as formatPlanDuration, g as formatPlanDurationLabel, u as formatRelativeTime, p as formatSubscriptionStatus };
57
+ export { a as formatBytes, v as formatCreditTransactionType, p as formatCredits, f as formatCurrency, s as formatDate, c as formatDateTime, y as formatInvoiceNumber, h as formatInvoiceStatus, l as formatNumber, u as formatPercentage, g as formatPlanDuration, _ as formatPlanDurationLabel, b as formatQuotaValue, d as formatRelativeTime, m as formatSubscriptionStatus };
55
58
 
56
59
  //# sourceMappingURL=format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","names":[],"sources":["../../../../src/billing/shared/utils/format.ts"],"sourcesContent":["/**\n * Formatting utilities for the billing module\n *\n * Re-exports common formatting utilities from fe-libs and adds billing-specific ones.\n */\n\nimport {\n SubscriptionStatus,\n InvoiceStatus,\n PaymentStatus,\n ChargeStatus,\n QuotaStatus,\n PlanDuration,\n PaymentMethodType,\n TransactionType,\n CreditTransactionType,\n} from '../types';\n\n// Re-export common formatting utilities from fe-libs\nexport {\n formatCurrency as formatCurrencyBase,\n formatCurrencyCompact as formatCurrencyCompactBase,\n formatDate,\n formatDateTime,\n formatRelativeTime,\n formatDuration,\n formatNumber,\n formatPercentage,\n formatBytes,\n formatCompactNumber,\n} from '@burdenoff/fe-libs/shared/utils';\n\n// Import for internal use\nimport {\n formatCurrency as formatCurrencyBase,\n formatCurrencyCompact as formatCurrencyCompactBase,\n} from '@burdenoff/fe-libs/shared/utils';\n\n// ============================================================================\n// Billing-specific Currency Formatting (handles CREDITS)\n// ============================================================================\n\nexport function formatCurrency(amount: number, currency = 'USD', locale = 'en-US'): string {\n // Handle CREDITS specially - credits are not a real currency\n if (currency.toUpperCase() === 'CREDITS') {\n return formatCredits(amount);\n }\n return formatCurrencyBase(amount, currency, locale);\n}\n\n/**\n * Format credits as \"X Credits\" or \"X.XX Credits\"\n * Credits are always displayed as a unit count, not as currency\n */\nexport function formatCredits(amount: number): string {\n // For whole numbers, show without decimals\n if (Number.isInteger(amount)) {\n return `${amount.toLocaleString()} Credits`;\n }\n // For fractional credits, show up to 2 decimals\n return `${amount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })} Credits`;\n}\n\nexport function formatCurrencyCompact(amount: number, currency = 'USD'): string {\n if (currency.toUpperCase() === 'CREDITS') {\n return formatCredits(amount);\n }\n return formatCurrencyCompactBase(amount, currency);\n}\n\n// ============================================================================\n// Status Formatting\n// ============================================================================\n\nexport function formatSubscriptionStatus(status: SubscriptionStatus): string {\n const labels: Record<SubscriptionStatus, string> = {\n [SubscriptionStatus.ACTIVE]: 'Active',\n [SubscriptionStatus.CANCELED]: 'Canceled',\n [SubscriptionStatus.EXPIRED]: 'Expired',\n [SubscriptionStatus.UPGRADED]: 'Upgraded',\n };\n return labels[status] || status;\n}\n\nexport function formatInvoiceStatus(status: InvoiceStatus): string {\n const labels: Record<InvoiceStatus, string> = {\n [InvoiceStatus.DRAFT]: 'Draft',\n [InvoiceStatus.OPEN]: 'Open',\n [InvoiceStatus.PAID]: 'Paid',\n [InvoiceStatus.VOID]: 'Void',\n [InvoiceStatus.UNCOLLECTIBLE]: 'Uncollectible',\n };\n return labels[status] || status;\n}\n\nexport function formatPaymentStatus(status: PaymentStatus): string {\n const labels: Record<PaymentStatus, string> = {\n [PaymentStatus.PENDING]: 'Pending',\n [PaymentStatus.SUCCEEDED]: 'Succeeded',\n [PaymentStatus.FAILED]: 'Failed',\n [PaymentStatus.CANCELED]: 'Canceled',\n [PaymentStatus.REFUNDED]: 'Refunded',\n };\n return labels[status] || status;\n}\n\nexport function formatChargeStatus(status: ChargeStatus): string {\n const labels: Record<ChargeStatus, string> = {\n [ChargeStatus.PENDING]: 'Pending',\n [ChargeStatus.SUCCEEDED]: 'Succeeded',\n [ChargeStatus.FAILED]: 'Failed',\n [ChargeStatus.REFUNDED]: 'Refunded',\n };\n return labels[status] || status;\n}\n\nexport function formatQuotaStatus(status: QuotaStatus): string {\n const labels: Record<QuotaStatus, string> = {\n [QuotaStatus.HEALTHY]: 'Healthy',\n [QuotaStatus.WARNING]: 'Warning',\n [QuotaStatus.CRITICAL]: 'Critical',\n [QuotaStatus.EXHAUSTED]: 'Exhausted',\n [QuotaStatus.INACTIVE]: 'Inactive',\n };\n return labels[status] || status;\n}\n\n// ============================================================================\n// Plan & Duration Formatting\n// ============================================================================\n\nexport function formatPlanDuration(duration: PlanDuration): string {\n const labels: Record<PlanDuration, string> = {\n [PlanDuration.MONTHLY]: 'month',\n [PlanDuration.YEARLY]: 'year',\n };\n return labels[duration] || duration;\n}\n\nexport function formatPlanDurationLabel(duration: PlanDuration): string {\n const labels: Record<PlanDuration, string> = {\n [PlanDuration.MONTHLY]: 'Monthly',\n [PlanDuration.YEARLY]: 'Yearly',\n };\n return labels[duration] || duration;\n}\n\nexport function formatPlanPrice(price: number, currency: string, duration: PlanDuration): string {\n return `${formatCurrency(price, currency)}/${formatPlanDuration(duration)}`;\n}\n\n// ============================================================================\n// Payment Method Formatting\n// ============================================================================\n\nexport function formatPaymentMethodType(type: PaymentMethodType): string {\n const labels: Record<PaymentMethodType, string> = {\n [PaymentMethodType.CREDIT_CARD]: 'Credit Card',\n [PaymentMethodType.DEBIT_CARD]: 'Debit Card',\n [PaymentMethodType.BANK_TRANSFER]: 'Bank Transfer',\n [PaymentMethodType.PAYPAL]: 'PayPal',\n [PaymentMethodType.CRYPTO]: 'Cryptocurrency',\n [PaymentMethodType.WALLET]: 'Wallet',\n [PaymentMethodType.NETBANKING]: 'Netbanking',\n };\n return labels[type] || type;\n}\n\nexport function formatCardBrand(brand?: string): string {\n if (!brand) return 'Card';\n return brand.charAt(0).toUpperCase() + brand.slice(1).toLowerCase();\n}\n\nexport function formatCardNumber(last4?: string): string {\n if (!last4) return '****';\n return `•••• ${last4}`;\n}\n\nexport function formatCardExpiry(month?: number, year?: number): string {\n if (!month || !year) return '';\n return `${month.toString().padStart(2, '0')}/${year.toString().slice(-2)}`;\n}\n\n// ============================================================================\n// Transaction Formatting\n// ============================================================================\n\nexport function formatTransactionType(type: TransactionType): string {\n const labels: Record<TransactionType, string> = {\n [TransactionType.PAYMENT]: 'Payment',\n [TransactionType.REFUND]: 'Refund',\n [TransactionType.ADJUSTMENT]: 'Adjustment',\n };\n return labels[type] || type;\n}\n\nexport function formatCreditTransactionType(type: CreditTransactionType): string {\n const labels: Record<CreditTransactionType, string> = {\n [CreditTransactionType.PURCHASE]: 'Purchase',\n [CreditTransactionType.USAGE]: 'Usage',\n [CreditTransactionType.ADJUSTMENT]: 'Earned',\n [CreditTransactionType.GRANTED]: 'Granted',\n };\n return labels[type] || type;\n}\n\n// ============================================================================\n// Text Formatting\n// ============================================================================\n\nexport function truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return `${text.slice(0, maxLength - 3)}...`;\n}\n\nexport function capitalizeFirst(text: string): string {\n if (!text) return '';\n return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();\n}\n\nexport function formatInvoiceNumber(number: string): string {\n return `#${number}`;\n}\n"],"mappings":";;;AA0CA,SAAgB,EAAe,GAAgB,IAAW,OAAO,IAAS,SAAiB;AAKzF,QAHI,EAAS,aAAa,KAAK,YACtB,EAAc,EAAO,GAEvB,EAAmB,GAAQ,GAAU,EAAO;;AAOrD,SAAgB,EAAc,GAAwB;AAMpD,QAJI,OAAO,UAAU,EAAO,GACnB,GAAG,EAAO,gBAAgB,CAAC,YAG7B,GAAG,EAAO,eAAe,KAAA,GAAW;EAAE,uBAAuB;EAAG,uBAAuB;EAAG,CAAC,CAAC;;AAcrG,SAAgB,EAAyB,GAAoC;AAO3E,QANmD;GAChD,EAAmB,SAAS;GAC5B,EAAmB,WAAW;GAC9B,EAAmB,UAAU;GAC7B,EAAmB,WAAW;EAChC,CACa,MAAW;;AAG3B,SAAgB,EAAoB,GAA+B;AAQjE,QAP8C;GAC3C,EAAc,QAAQ;GACtB,EAAc,OAAO;GACrB,EAAc,OAAO;GACrB,EAAc,OAAO;GACrB,EAAc,gBAAgB;EAChC,CACa,MAAW;;AAuC3B,SAAgB,EAAmB,GAAgC;AAKjE,QAJ6C;GAC1C,EAAa,UAAU;GACvB,EAAa,SAAS;EACxB,CACa,MAAa;;AAG7B,SAAgB,EAAwB,GAAgC;AAKtE,QAJ6C;GAC1C,EAAa,UAAU;GACvB,EAAa,SAAS;EACxB,CACa,MAAa;;AAoD7B,SAAgB,EAA4B,GAAqC;AAO/E,QANsD;GACnD,EAAsB,WAAW;GACjC,EAAsB,QAAQ;GAC9B,EAAsB,aAAa;GACnC,EAAsB,UAAU;EAClC,CACa,MAAS;;AAiBzB,SAAgB,EAAoB,GAAwB;AAC1D,QAAO,IAAI"}
1
+ {"version":3,"file":"format.js","names":[],"sources":["../../../../src/billing/shared/utils/format.ts"],"sourcesContent":["/**\n * Formatting utilities for the billing module\n *\n * Re-exports common formatting utilities from fe-libs and adds billing-specific ones.\n */\n\nimport {\n SubscriptionStatus,\n InvoiceStatus,\n PaymentStatus,\n ChargeStatus,\n QuotaStatus,\n PlanDuration,\n PaymentMethodType,\n TransactionType,\n CreditTransactionType,\n} from '../types';\n\n// Re-export common formatting utilities from fe-libs\nexport {\n formatCurrency as formatCurrencyBase,\n formatCurrencyCompact as formatCurrencyCompactBase,\n formatDate,\n formatDateTime,\n formatRelativeTime,\n formatDuration,\n formatNumber,\n formatPercentage,\n formatBytes,\n formatCompactNumber,\n} from '@burdenoff/fe-libs/shared/utils';\n\n// Import for internal use\nimport {\n formatCurrency as formatCurrencyBase,\n formatCurrencyCompact as formatCurrencyCompactBase,\n formatBytes,\n} from '@burdenoff/fe-libs/shared/utils';\n\n// ============================================================================\n// Billing-specific Currency Formatting (handles CREDITS)\n// ============================================================================\n\nexport function formatCurrency(amount: number, currency = 'USD', locale = 'en-US'): string {\n // Handle CREDITS specially - credits are not a real currency\n if (currency.toUpperCase() === 'CREDITS') {\n return formatCredits(amount);\n }\n return formatCurrencyBase(amount, currency, locale);\n}\n\n/**\n * Format credits as \"X Credits\" or \"X.XX Credits\"\n * Credits are always displayed as a unit count, not as currency\n */\nexport function formatCredits(amount: number): string {\n // For whole numbers, show without decimals\n if (Number.isInteger(amount)) {\n return `${amount.toLocaleString()} Credits`;\n }\n // For fractional credits, show up to 2 decimals\n return `${amount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })} Credits`;\n}\n\nexport function formatCurrencyCompact(amount: number, currency = 'USD'): string {\n if (currency.toUpperCase() === 'CREDITS') {\n return formatCredits(amount);\n }\n return formatCurrencyCompactBase(amount, currency);\n}\n\n// ============================================================================\n// Status Formatting\n// ============================================================================\n\nexport function formatSubscriptionStatus(status: SubscriptionStatus): string {\n const labels: Record<SubscriptionStatus, string> = {\n [SubscriptionStatus.ACTIVE]: 'Active',\n [SubscriptionStatus.CANCELED]: 'Canceled',\n [SubscriptionStatus.EXPIRED]: 'Expired',\n [SubscriptionStatus.UPGRADED]: 'Upgraded',\n };\n return labels[status] || status;\n}\n\nexport function formatInvoiceStatus(status: InvoiceStatus): string {\n const labels: Record<InvoiceStatus, string> = {\n [InvoiceStatus.DRAFT]: 'Draft',\n [InvoiceStatus.OPEN]: 'Open',\n [InvoiceStatus.PAID]: 'Paid',\n [InvoiceStatus.VOID]: 'Void',\n [InvoiceStatus.UNCOLLECTIBLE]: 'Uncollectible',\n };\n return labels[status] || status;\n}\n\nexport function formatPaymentStatus(status: PaymentStatus): string {\n const labels: Record<PaymentStatus, string> = {\n [PaymentStatus.PENDING]: 'Pending',\n [PaymentStatus.SUCCEEDED]: 'Succeeded',\n [PaymentStatus.FAILED]: 'Failed',\n [PaymentStatus.CANCELED]: 'Canceled',\n [PaymentStatus.REFUNDED]: 'Refunded',\n };\n return labels[status] || status;\n}\n\nexport function formatChargeStatus(status: ChargeStatus): string {\n const labels: Record<ChargeStatus, string> = {\n [ChargeStatus.PENDING]: 'Pending',\n [ChargeStatus.SUCCEEDED]: 'Succeeded',\n [ChargeStatus.FAILED]: 'Failed',\n [ChargeStatus.REFUNDED]: 'Refunded',\n };\n return labels[status] || status;\n}\n\nexport function formatQuotaStatus(status: QuotaStatus): string {\n const labels: Record<QuotaStatus, string> = {\n [QuotaStatus.HEALTHY]: 'Healthy',\n [QuotaStatus.WARNING]: 'Warning',\n [QuotaStatus.CRITICAL]: 'Critical',\n [QuotaStatus.EXHAUSTED]: 'Exhausted',\n [QuotaStatus.INACTIVE]: 'Inactive',\n };\n return labels[status] || status;\n}\n\n// ============================================================================\n// Plan & Duration Formatting\n// ============================================================================\n\nexport function formatPlanDuration(duration: PlanDuration): string {\n const labels: Record<PlanDuration, string> = {\n [PlanDuration.MONTHLY]: 'month',\n [PlanDuration.YEARLY]: 'year',\n };\n return labels[duration] || duration;\n}\n\nexport function formatPlanDurationLabel(duration: PlanDuration): string {\n const labels: Record<PlanDuration, string> = {\n [PlanDuration.MONTHLY]: 'Monthly',\n [PlanDuration.YEARLY]: 'Yearly',\n };\n return labels[duration] || duration;\n}\n\nexport function formatPlanPrice(price: number, currency: string, duration: PlanDuration): string {\n return `${formatCurrency(price, currency)}/${formatPlanDuration(duration)}`;\n}\n\n// ============================================================================\n// Payment Method Formatting\n// ============================================================================\n\nexport function formatPaymentMethodType(type: PaymentMethodType): string {\n const labels: Record<PaymentMethodType, string> = {\n [PaymentMethodType.CREDIT_CARD]: 'Credit Card',\n [PaymentMethodType.DEBIT_CARD]: 'Debit Card',\n [PaymentMethodType.BANK_TRANSFER]: 'Bank Transfer',\n [PaymentMethodType.PAYPAL]: 'PayPal',\n [PaymentMethodType.CRYPTO]: 'Cryptocurrency',\n [PaymentMethodType.WALLET]: 'Wallet',\n [PaymentMethodType.NETBANKING]: 'Netbanking',\n };\n return labels[type] || type;\n}\n\nexport function formatCardBrand(brand?: string): string {\n if (!brand) return 'Card';\n return brand.charAt(0).toUpperCase() + brand.slice(1).toLowerCase();\n}\n\nexport function formatCardNumber(last4?: string): string {\n if (!last4) return '****';\n return `•••• ${last4}`;\n}\n\nexport function formatCardExpiry(month?: number, year?: number): string {\n if (!month || !year) return '';\n return `${month.toString().padStart(2, '0')}/${year.toString().slice(-2)}`;\n}\n\n// ============================================================================\n// Transaction Formatting\n// ============================================================================\n\nexport function formatTransactionType(type: TransactionType): string {\n const labels: Record<TransactionType, string> = {\n [TransactionType.PAYMENT]: 'Payment',\n [TransactionType.REFUND]: 'Refund',\n [TransactionType.ADJUSTMENT]: 'Adjustment',\n };\n return labels[type] || type;\n}\n\nexport function formatCreditTransactionType(type: CreditTransactionType): string {\n const labels: Record<CreditTransactionType, string> = {\n [CreditTransactionType.PURCHASE]: 'Purchase',\n [CreditTransactionType.USAGE]: 'Usage',\n [CreditTransactionType.ADJUSTMENT]: 'Earned',\n [CreditTransactionType.GRANTED]: 'Granted',\n };\n return labels[type] || type;\n}\n\n// ============================================================================\n// Text Formatting\n// ============================================================================\n\nexport function truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return `${text.slice(0, maxLength - 3)}...`;\n}\n\nexport function capitalizeFirst(text: string): string {\n if (!text) return '';\n return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();\n}\n\nexport function formatInvoiceNumber(number: string): string {\n return `#${number}`;\n}\n\n// ============================================================================\n// Quota Value Formatting\n// ============================================================================\n\n/**\n * Format a quota's total value for display, unit-aware. Byte-denominated\n * quotas (storage, upload caps) render as MB/GB via formatBytes instead of a\n * raw byte count — everything else falls back to a plain localized number.\n */\nexport function formatQuotaValue(value: number, unit?: string): string {\n if (unit === 'bytes') return formatBytes(value);\n return value.toLocaleString();\n}\n"],"mappings":";;;AA2CA,SAAgB,EAAe,GAAgB,IAAW,OAAO,IAAS,SAAiB;AAKzF,QAHI,EAAS,aAAa,KAAK,YACtB,EAAc,EAAO,GAEvB,EAAmB,GAAQ,GAAU,EAAO;;AAOrD,SAAgB,EAAc,GAAwB;AAMpD,QAJI,OAAO,UAAU,EAAO,GACnB,GAAG,EAAO,gBAAgB,CAAC,YAG7B,GAAG,EAAO,eAAe,KAAA,GAAW;EAAE,uBAAuB;EAAG,uBAAuB;EAAG,CAAC,CAAC;;AAcrG,SAAgB,EAAyB,GAAoC;AAO3E,QANmD;GAChD,EAAmB,SAAS;GAC5B,EAAmB,WAAW;GAC9B,EAAmB,UAAU;GAC7B,EAAmB,WAAW;EAChC,CACa,MAAW;;AAG3B,SAAgB,EAAoB,GAA+B;AAQjE,QAP8C;GAC3C,EAAc,QAAQ;GACtB,EAAc,OAAO;GACrB,EAAc,OAAO;GACrB,EAAc,OAAO;GACrB,EAAc,gBAAgB;EAChC,CACa,MAAW;;AAuC3B,SAAgB,EAAmB,GAAgC;AAKjE,QAJ6C;GAC1C,EAAa,UAAU;GACvB,EAAa,SAAS;EACxB,CACa,MAAa;;AAG7B,SAAgB,EAAwB,GAAgC;AAKtE,QAJ6C;GAC1C,EAAa,UAAU;GACvB,EAAa,SAAS;EACxB,CACa,MAAa;;AAoD7B,SAAgB,EAA4B,GAAqC;AAO/E,QANsD;GACnD,EAAsB,WAAW;GACjC,EAAsB,QAAQ;GAC9B,EAAsB,aAAa;GACnC,EAAsB,UAAU;EAClC,CACa,MAAS;;AAiBzB,SAAgB,EAAoB,GAAwB;AAC1D,QAAO,IAAI;;AAYb,SAAgB,EAAiB,GAAe,GAAuB;AAErE,QADI,MAAS,UAAgB,EAAY,EAAM,GACxC,EAAM,gBAAgB"}
@@ -5,7 +5,31 @@ function e(e) {
5
5
  let n = e.quota?.limits;
6
6
  return typeof n?.value == "number" ? n.value : 0;
7
7
  }
8
+ function t(e) {
9
+ let t = e.context;
10
+ return typeof t?.unit == "string" ? t.unit : void 0;
11
+ }
12
+ var n = new Set([
13
+ "count",
14
+ "bytes",
15
+ "days",
16
+ "seconds",
17
+ "hours",
18
+ "monthly",
19
+ "daily",
20
+ "concurrent"
21
+ ]);
22
+ function r(e) {
23
+ let t = e.split(".").filter(Boolean), r = t.length > 1 ? t.slice(1) : t, i = r[r.length - 1], a = (r.length > 1 && i && n.has(i) ? r.slice(0, -1) : r).join(" ").replace(/_/g, " ").trim().split(/\s+/).filter(Boolean);
24
+ return a.length === 0 ? e : a.map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" ");
25
+ }
26
+ function i(e) {
27
+ let t = e.quota?.context;
28
+ if (t && typeof t.displayName == "string" && t.displayName) return t.displayName;
29
+ let n = e.quota?.name;
30
+ return n ? r(n) : "Unknown Quota";
31
+ }
8
32
  //#endregion
9
- export { e as getPlanFeatureQuantity };
33
+ export { e as getPlanFeatureQuantity, t as getPlanFeatureUnit, i as getQuotaDisplayName, r as humanizeQuotaName };
10
34
 
11
35
  //# sourceMappingURL=planFeatureQuantity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"planFeatureQuantity.js","names":[],"sources":["../../../../src/billing/shared/utils/planFeatureQuantity.ts"],"sourcesContent":["/**\n * Source of truth for \"how big is this quota on this plan\":\n * 1. Per-plan override stored on the SubscriptionFeature row's\n * `context.quantity` (autoseed + post-OSS-release createPlan path).\n * 2. Fall back to the catalog default `quota.limits.value` for plans that\n * pre-date per-plan overrides (only the autoseeded Free plan today).\n */\ntype FeatureLike = {\n context?: unknown;\n quota?: { limits?: unknown } | null;\n};\n\nexport function getPlanFeatureQuantity(feature: FeatureLike): number {\n const ctx = feature.context as { quantity?: number } | null | undefined;\n if (ctx && typeof ctx.quantity === 'number') return ctx.quantity;\n\n const limits = feature.quota?.limits as { value?: number } | null | undefined;\n return typeof limits?.value === 'number' ? limits.value : 0;\n}\n"],"mappings":";AAYA,SAAgB,EAAuB,GAA8B;CACnE,IAAM,IAAM,EAAQ;AACpB,KAAI,KAAO,OAAO,EAAI,YAAa,SAAU,QAAO,EAAI;CAExD,IAAM,IAAS,EAAQ,OAAO;AAC9B,QAAO,OAAO,GAAQ,SAAU,WAAW,EAAO,QAAQ"}
1
+ {"version":3,"file":"planFeatureQuantity.js","names":[],"sources":["../../../../src/billing/shared/utils/planFeatureQuantity.ts"],"sourcesContent":["/**\n * Source of truth for \"how big is this quota on this plan\":\n * 1. Per-plan override stored on the SubscriptionFeature row's\n * `context.quantity` (autoseed + post-OSS-release createPlan path).\n * 2. Fall back to the catalog default `quota.limits.value` for plans that\n * pre-date per-plan overrides (only the autoseeded Free plan today).\n */\ntype FeatureLike = {\n context?: unknown;\n quota?: { name?: string | null; limits?: unknown; context?: unknown } | null;\n};\n\nexport function getPlanFeatureQuantity(feature: FeatureLike): number {\n const ctx = feature.context as { quantity?: number } | null | undefined;\n if (ctx && typeof ctx.quantity === 'number') return ctx.quantity;\n\n const limits = feature.quota?.limits as { value?: number } | null | undefined;\n return typeof limits?.value === 'number' ? limits.value : 0;\n}\n\n/**\n * The unit a quota's quantity is denominated in (e.g. \"bytes\", \"days\",\n * \"workflows\"), as seeded on the SubscriptionFeature row's `context.unit`.\n * Used to render byte-denominated quotas (storage, uploads) as MB/GB instead\n * of a raw byte count — see formatQuotaValue in format.ts.\n */\nexport function getPlanFeatureUnit(feature: FeatureLike): string | undefined {\n const ctx = feature.context as { unit?: string } | null | undefined;\n return typeof ctx?.unit === 'string' ? ctx.unit : undefined;\n}\n\n// Trailing name segments that describe the unit/cadence, not the quota\n// itself (e.g. \"fluidgrids.storage.bytes\" → the \"bytes\" segment). Stripped\n// by humanizeQuotaName so the fallback label doesn't end in a redundant\n// technical word.\nconst NAME_SUFFIX_STOPWORDS = new Set([\n 'count',\n 'bytes',\n 'days',\n 'seconds',\n 'hours',\n 'monthly',\n 'daily',\n 'concurrent',\n]);\n\n/**\n * Best-effort human label for a quota that has no seeded `displayName`\n * (older rows only — every quota seeded since BOFF-3058 carries one). Turns\n * \"fluidgrids.scheduled_triggers.count\" into \"Scheduled Triggers\": drops the\n * leading product/boundary segment and a trailing unit/cadence stopword,\n * then title-cases the rest.\n */\nexport function humanizeQuotaName(name: string): string {\n const segments = name.split('.').filter(Boolean);\n const withoutBoundary = segments.length > 1 ? segments.slice(1) : segments;\n const last = withoutBoundary[withoutBoundary.length - 1];\n const meaningful =\n withoutBoundary.length > 1 && last && NAME_SUFFIX_STOPWORDS.has(last)\n ? withoutBoundary.slice(0, -1)\n : withoutBoundary;\n\n const words = meaningful.join(' ').replace(/_/g, ' ').trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) return name;\n return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');\n}\n\n/**\n * The name to show a user for a quota: prefers the curated `displayName`\n * seeded on Quota.context (e.g. \"Storage\", \"Team Members\"); falls back to a\n * humanized version of the raw dotted identifier for any quota seeded before\n * displayName was added to the catalog.\n */\nexport function getQuotaDisplayName(feature: FeatureLike): string {\n const quotaContext = feature.quota?.context as { displayName?: string } | null | undefined;\n if (quotaContext && typeof quotaContext.displayName === 'string' && quotaContext.displayName) {\n return quotaContext.displayName;\n }\n\n const rawName = feature.quota?.name;\n return rawName ? humanizeQuotaName(rawName) : 'Unknown Quota';\n}\n"],"mappings":";AAYA,SAAgB,EAAuB,GAA8B;CACnE,IAAM,IAAM,EAAQ;AACpB,KAAI,KAAO,OAAO,EAAI,YAAa,SAAU,QAAO,EAAI;CAExD,IAAM,IAAS,EAAQ,OAAO;AAC9B,QAAO,OAAO,GAAQ,SAAU,WAAW,EAAO,QAAQ;;AAS5D,SAAgB,EAAmB,GAA0C;CAC3E,IAAM,IAAM,EAAQ;AACpB,QAAO,OAAO,GAAK,QAAS,WAAW,EAAI,OAAO,KAAA;;AAOpD,IAAM,IAAwB,IAAI,IAAI;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AASF,SAAgB,EAAkB,GAAsB;CACtD,IAAM,IAAW,EAAK,MAAM,IAAI,CAAC,OAAO,QAAQ,EAC1C,IAAkB,EAAS,SAAS,IAAI,EAAS,MAAM,EAAE,GAAG,GAC5D,IAAO,EAAgB,EAAgB,SAAS,IAMhD,KAJJ,EAAgB,SAAS,KAAK,KAAQ,EAAsB,IAAI,EAAK,GACjE,EAAgB,MAAM,GAAG,GAAG,GAC5B,GAEmB,KAAK,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAEzF,QADI,EAAM,WAAW,IAAU,IACxB,EAAM,KAAK,MAAM,EAAE,OAAO,EAAE,CAAC,aAAa,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI;;AAS3E,SAAgB,EAAoB,GAA8B;CAChE,IAAM,IAAe,EAAQ,OAAO;AACpC,KAAI,KAAgB,OAAO,EAAa,eAAgB,YAAY,EAAa,YAC/E,QAAO,EAAa;CAGtB,IAAM,IAAU,EAAQ,OAAO;AAC/B,QAAO,IAAU,EAAkB,EAAQ,GAAG"}
@@ -405,6 +405,7 @@ var y = t`
405
405
  limits
406
406
  reusable
407
407
  quotaType
408
+ context
408
409
  }
409
410
  }
410
411
  itemEntitlements {