@umituz/web-dashboard 2.2.0 → 2.3.0
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/package.json +12 -1
- package/src/domains/billing/components/BillingLayout.tsx +63 -0
- package/src/domains/billing/components/BillingPortal.tsx +198 -0
- package/src/domains/billing/components/InvoiceCard.tsx +143 -0
- package/src/domains/billing/components/PaymentMethodsList.tsx +116 -0
- package/src/domains/billing/components/PlanComparison.tsx +199 -0
- package/src/domains/billing/components/UsageCard.tsx +103 -0
- package/src/domains/billing/components/index.ts +12 -0
- package/src/domains/billing/hooks/index.ts +7 -0
- package/src/domains/billing/hooks/useBilling.ts +385 -0
- package/src/domains/billing/index.ts +69 -0
- package/src/domains/billing/types/billing.ts +347 -0
- package/src/domains/billing/types/index.ts +28 -0
- package/src/domains/billing/utils/billing.ts +344 -0
- package/src/domains/billing/utils/index.ts +29 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for billing operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
BillingCycle,
|
|
9
|
+
Currency,
|
|
10
|
+
PlanTier,
|
|
11
|
+
Subscription,
|
|
12
|
+
PaymentMethod,
|
|
13
|
+
Invoice,
|
|
14
|
+
InvoiceStatus,
|
|
15
|
+
UsageMetric,
|
|
16
|
+
} from "../types/billing";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format price with currency
|
|
20
|
+
*
|
|
21
|
+
* @param amount - Amount to format
|
|
22
|
+
* @param currency - Currency code (default: USD)
|
|
23
|
+
* @returns Formatted price string
|
|
24
|
+
*/
|
|
25
|
+
export function formatPrice(amount: number, currency: Currency = "USD"): string {
|
|
26
|
+
const localeMap: Record<Currency, string> = {
|
|
27
|
+
USD: "en-US",
|
|
28
|
+
EUR: "de-DE",
|
|
29
|
+
GBP: "en-GB",
|
|
30
|
+
TRY: "tr-TR",
|
|
31
|
+
JPY: "ja-JP",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return new Intl.NumberFormat(localeMap[currency], {
|
|
35
|
+
style: "currency",
|
|
36
|
+
currency,
|
|
37
|
+
}).format(amount);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Calculate yearly discount
|
|
42
|
+
*
|
|
43
|
+
* @param monthlyPrice - Monthly price
|
|
44
|
+
* @param yearlyPrice - Yearly price
|
|
45
|
+
* @returns Discount percentage
|
|
46
|
+
*/
|
|
47
|
+
export function calculateDiscount(monthlyPrice: number, yearlyPrice: number): number {
|
|
48
|
+
const yearlyMonthly = yearlyPrice / 12;
|
|
49
|
+
const discount = ((monthlyPrice - yearlyMonthly) / monthlyPrice) * 100;
|
|
50
|
+
return Math.round(discount);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate prorated amount
|
|
55
|
+
*
|
|
56
|
+
* @param amount - Full amount
|
|
57
|
+
* @param daysUsed - Days used in period
|
|
58
|
+
* @param totalDays - Total days in period
|
|
59
|
+
* @returns Prorated amount
|
|
60
|
+
*/
|
|
61
|
+
export function calculateProratedAmount(
|
|
62
|
+
amount: number,
|
|
63
|
+
daysUsed: number,
|
|
64
|
+
totalDays: number
|
|
65
|
+
): number {
|
|
66
|
+
if (totalDays === 0) return 0;
|
|
67
|
+
return (amount / totalDays) * daysUsed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get plan price by cycle
|
|
72
|
+
*
|
|
73
|
+
* @param plan - Plan tier
|
|
74
|
+
* @param cycle - Billing cycle
|
|
75
|
+
* @returns Price for cycle
|
|
76
|
+
*/
|
|
77
|
+
export function getPlanPrice(plan: PlanTier, cycle: BillingCycle): number {
|
|
78
|
+
return cycle === "monthly" ? plan.monthlyPrice : plan.yearlyPrice;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculate usage percentage
|
|
83
|
+
*
|
|
84
|
+
* @param metric - Usage metric
|
|
85
|
+
* @returns Usage percentage (0-100)
|
|
86
|
+
*/
|
|
87
|
+
export function calculateUsagePercentage(metric: UsageMetric): number {
|
|
88
|
+
if (metric.limit === 0) return 0;
|
|
89
|
+
return Math.min((metric.current / metric.limit) * 100, 100);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if usage is near limit
|
|
94
|
+
*
|
|
95
|
+
* @param metric - Usage metric
|
|
96
|
+
* @param threshold - Warning threshold (default: 80)
|
|
97
|
+
* @returns Whether near limit
|
|
98
|
+
*/
|
|
99
|
+
export function isNearLimit(metric: UsageMetric, threshold: number = 80): boolean {
|
|
100
|
+
return calculateUsagePercentage(metric) >= threshold;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get subscription status color
|
|
105
|
+
*
|
|
106
|
+
* @param status - Subscription status
|
|
107
|
+
* @returns Color class
|
|
108
|
+
*/
|
|
109
|
+
export function getStatusColor(status: SubscriptionStatus): string {
|
|
110
|
+
const colorMap: Record<SubscriptionStatus, string> = {
|
|
111
|
+
active: "text-green-600 dark:text-green-500",
|
|
112
|
+
trialing: "text-blue-600 dark:text-blue-500",
|
|
113
|
+
past_due: "text-orange-600 dark:text-orange-500",
|
|
114
|
+
canceled: "text-gray-600 dark:text-gray-500",
|
|
115
|
+
unpaid: "text-red-600 dark:text-red-500",
|
|
116
|
+
incomplete: "text-yellow-600 dark:text-yellow-500",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return colorMap[status] || "text-gray-600";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get subscription status label
|
|
124
|
+
*
|
|
125
|
+
* @param status - Subscription status
|
|
126
|
+
* @returns Human readable label
|
|
127
|
+
*/
|
|
128
|
+
export function getStatusLabel(status: SubscriptionStatus): string {
|
|
129
|
+
const labelMap: Record<SubscriptionStatus, string> = {
|
|
130
|
+
active: "Active",
|
|
131
|
+
trialing: "Trial",
|
|
132
|
+
past_due: "Past Due",
|
|
133
|
+
canceled: "Canceled",
|
|
134
|
+
unpaid: "Unpaid",
|
|
135
|
+
incomplete: "Incomplete",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return labelMap[status] || status;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get invoice status color
|
|
143
|
+
*
|
|
144
|
+
* @param status - Invoice status
|
|
145
|
+
* @returns Color class
|
|
146
|
+
*/
|
|
147
|
+
export function getInvoiceStatusColor(status: InvoiceStatus): string {
|
|
148
|
+
const colorMap: Record<InvoiceStatus, string> = {
|
|
149
|
+
draft: "text-gray-600 dark:text-gray-500",
|
|
150
|
+
open: "text-orange-600 dark:text-orange-500",
|
|
151
|
+
paid: "text-green-600 dark:text-green-500",
|
|
152
|
+
void: "text-gray-600 dark:text-gray-500",
|
|
153
|
+
uncollectible: "text-red-600 dark:text-red-500",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return colorMap[status] || "text-gray-600";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get invoice status label
|
|
161
|
+
*
|
|
162
|
+
* @param status - Invoice status
|
|
163
|
+
* @returns Human readable label
|
|
164
|
+
*/
|
|
165
|
+
export function getInvoiceStatusLabel(status: InvoiceStatus): string {
|
|
166
|
+
const labelMap: Record<InvoiceStatus, string> = {
|
|
167
|
+
draft: "Draft",
|
|
168
|
+
open: "Open",
|
|
169
|
+
paid: "Paid",
|
|
170
|
+
void: "Void",
|
|
171
|
+
uncollectible: "Uncollectible",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return labelMap[status] || status;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format card number
|
|
179
|
+
*
|
|
180
|
+
* @param last4 - Last 4 digits
|
|
181
|
+
* @param brand - Card brand
|
|
182
|
+
* @returns Formatted card display
|
|
183
|
+
*/
|
|
184
|
+
export function formatCardNumber(last4: string, brand: string): string {
|
|
185
|
+
return `${brand.toUpperCase()} •••• ${last4}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Format expiry date
|
|
190
|
+
*
|
|
191
|
+
* @param month - Expiry month
|
|
192
|
+
* @param year - Expiry year
|
|
193
|
+
* @returns Formatted expiry (MM/YY)
|
|
194
|
+
*/
|
|
195
|
+
export function formatExpiry(month: number, year: number): string {
|
|
196
|
+
return `${String(month).padStart(2, "0")}/${String(year).slice(-2)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Calculate remaining days
|
|
201
|
+
*
|
|
202
|
+
* @param endDate - End date string
|
|
203
|
+
* @returns Days remaining
|
|
204
|
+
*/
|
|
205
|
+
export function getDaysRemaining(endDate: string): number {
|
|
206
|
+
const end = new Date(endDate);
|
|
207
|
+
const now = new Date();
|
|
208
|
+
const diff = end.getTime() - now.getTime();
|
|
209
|
+
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if trial is expiring soon
|
|
214
|
+
*
|
|
215
|
+
* @param trialEnd - Trial end date
|
|
216
|
+
* @param daysThreshold - Days threshold (default: 7)
|
|
217
|
+
* @returns Whether trial is expiring soon
|
|
218
|
+
*/
|
|
219
|
+
export function isTrialExpiringSoon(trialEnd: string, daysThreshold: number = 7): boolean {
|
|
220
|
+
const daysRemaining = getDaysRemaining(trialEnd);
|
|
221
|
+
return daysRemaining > 0 && daysRemaining <= daysThreshold;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate next billing date
|
|
226
|
+
*
|
|
227
|
+
* @param currentPeriodEnd - Current period end
|
|
228
|
+
* @param cycle - Billing cycle
|
|
229
|
+
* @returns Next billing date
|
|
230
|
+
*/
|
|
231
|
+
export function getNextBillingDate(
|
|
232
|
+
currentPeriodEnd: string,
|
|
233
|
+
cycle: BillingCycle
|
|
234
|
+
): Date {
|
|
235
|
+
const nextDate = new Date(currentPeriodEnd);
|
|
236
|
+
|
|
237
|
+
if (cycle === "monthly") {
|
|
238
|
+
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
239
|
+
} else {
|
|
240
|
+
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return nextDate;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Group invoices by status
|
|
248
|
+
*
|
|
249
|
+
* @param invoices - Array of invoices
|
|
250
|
+
* @returns Grouped invoices map
|
|
251
|
+
*/
|
|
252
|
+
export function groupInvoicesByStatus(
|
|
253
|
+
invoices: Invoice[]
|
|
254
|
+
): Record<InvoiceStatus, Invoice[]> {
|
|
255
|
+
const grouped: Record<string, Invoice[]> = {
|
|
256
|
+
draft: [],
|
|
257
|
+
open: [],
|
|
258
|
+
paid: [],
|
|
259
|
+
void: [],
|
|
260
|
+
uncollectible: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
invoices.forEach((invoice) => {
|
|
264
|
+
if (grouped[invoice.status]) {
|
|
265
|
+
grouped[invoice.status].push(invoice);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return grouped as Record<InvoiceStatus, Invoice[]>;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Calculate total invoice amount
|
|
274
|
+
*
|
|
275
|
+
* @param invoices - Array of invoices
|
|
276
|
+
* @param status - Optional status filter
|
|
277
|
+
* @returns Total amount
|
|
278
|
+
*/
|
|
279
|
+
export function calculateInvoiceTotal(
|
|
280
|
+
invoices: Invoice[],
|
|
281
|
+
status?: InvoiceStatus
|
|
282
|
+
): number {
|
|
283
|
+
const filtered = status ? invoices.filter((inv) => inv.status === status) : invoices;
|
|
284
|
+
return filtered.reduce((sum, inv) => sum + inv.amount, 0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Sort invoices by date
|
|
289
|
+
*
|
|
290
|
+
* @param invoices - Array of invoices
|
|
291
|
+
* @param order - Sort order (default: desc)
|
|
292
|
+
* @returns Sorted invoices
|
|
293
|
+
*/
|
|
294
|
+
export function sortInvoicesByDate(
|
|
295
|
+
invoices: Invoice[],
|
|
296
|
+
order: "asc" | "desc" = "desc"
|
|
297
|
+
): Invoice[] {
|
|
298
|
+
return [...invoices].sort((a, b) => {
|
|
299
|
+
const dateA = new Date(a.date).getTime();
|
|
300
|
+
const dateB = new Date(b.date).getTime();
|
|
301
|
+
return order === "asc" ? dateA - dateB : dateB - dateB;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Format feature list item
|
|
307
|
+
*
|
|
308
|
+
* @param feature - Feature item (string or object)
|
|
309
|
+
* @returns Formatted feature
|
|
310
|
+
*/
|
|
311
|
+
export function formatFeature(
|
|
312
|
+
feature: string | { text: string; bold?: boolean; included?: boolean }
|
|
313
|
+
): { text: string; bold?: boolean; included?: boolean } {
|
|
314
|
+
if (typeof feature === "string") {
|
|
315
|
+
return { text: feature, included: true };
|
|
316
|
+
}
|
|
317
|
+
return feature;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if plan is popular
|
|
322
|
+
*
|
|
323
|
+
* @param plan - Plan tier
|
|
324
|
+
* @param plans - All available plans
|
|
325
|
+
* @returns Whether plan is popular (middle tier)
|
|
326
|
+
*/
|
|
327
|
+
export function isPopularPlan(plan: PlanTier, plans: PlanTier[]): boolean {
|
|
328
|
+
const middleIndex = Math.floor(plans.length / 2);
|
|
329
|
+
return plans[middleIndex]?.id === plan.id;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate trial days text
|
|
334
|
+
*
|
|
335
|
+
* @param trialEnd - Trial end date
|
|
336
|
+
* @returns Days remaining text
|
|
337
|
+
*/
|
|
338
|
+
export function getTrialDaysText(trialEnd: string): string {
|
|
339
|
+
const days = getDaysRemaining(trialEnd);
|
|
340
|
+
|
|
341
|
+
if (days === 0) return "Trial ends today";
|
|
342
|
+
if (days === 1) return "1 day left";
|
|
343
|
+
return `${days} days left`;
|
|
344
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Export all billing utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
formatPrice,
|
|
9
|
+
calculateDiscount,
|
|
10
|
+
calculateProratedAmount,
|
|
11
|
+
getPlanPrice,
|
|
12
|
+
calculateUsagePercentage,
|
|
13
|
+
isNearLimit,
|
|
14
|
+
getStatusColor,
|
|
15
|
+
getStatusLabel,
|
|
16
|
+
getInvoiceStatusColor,
|
|
17
|
+
getInvoiceStatusLabel,
|
|
18
|
+
formatCardNumber,
|
|
19
|
+
formatExpiry,
|
|
20
|
+
getDaysRemaining,
|
|
21
|
+
isTrialExpiringSoon,
|
|
22
|
+
getNextBillingDate,
|
|
23
|
+
groupInvoicesByStatus,
|
|
24
|
+
calculateInvoiceTotal,
|
|
25
|
+
sortInvoicesByDate,
|
|
26
|
+
formatFeature,
|
|
27
|
+
isPopularPlan,
|
|
28
|
+
getTrialDaysText,
|
|
29
|
+
} from "./billing";
|