@umituz/react-native-subscription 1.0.7 → 1.1.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.
Files changed (83) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/lib/application/ports/ISubscriptionRepository.d.ts +25 -0
  4. package/lib/application/ports/ISubscriptionRepository.d.ts.map +1 -0
  5. package/lib/application/ports/ISubscriptionRepository.js +9 -0
  6. package/lib/application/ports/ISubscriptionRepository.js.map +1 -0
  7. package/lib/application/ports/ISubscriptionService.d.ts +28 -0
  8. package/lib/application/ports/ISubscriptionService.d.ts.map +1 -0
  9. package/lib/application/ports/ISubscriptionService.js +6 -0
  10. package/lib/application/ports/ISubscriptionService.js.map +1 -0
  11. package/lib/domain/entities/SubscriptionStatus.d.ts +31 -0
  12. package/lib/domain/entities/SubscriptionStatus.d.ts.map +1 -0
  13. package/lib/domain/entities/SubscriptionStatus.js +39 -0
  14. package/lib/domain/entities/SubscriptionStatus.js.map +1 -0
  15. package/lib/domain/errors/SubscriptionError.d.ts +18 -0
  16. package/lib/domain/errors/SubscriptionError.d.ts.map +1 -0
  17. package/lib/domain/errors/SubscriptionError.js +30 -0
  18. package/lib/domain/errors/SubscriptionError.js.map +1 -0
  19. package/lib/domain/value-objects/SubscriptionConfig.d.ts +15 -0
  20. package/lib/domain/value-objects/SubscriptionConfig.d.ts.map +1 -0
  21. package/lib/domain/value-objects/SubscriptionConfig.js +6 -0
  22. package/lib/domain/value-objects/SubscriptionConfig.js.map +1 -0
  23. package/lib/index.d.ts +33 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +43 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/infrastructure/services/ActivationHandler.d.ts +20 -0
  28. package/lib/infrastructure/services/ActivationHandler.d.ts.map +1 -0
  29. package/lib/infrastructure/services/ActivationHandler.js +71 -0
  30. package/lib/infrastructure/services/ActivationHandler.js.map +1 -0
  31. package/lib/infrastructure/services/SubscriptionService.d.ts +22 -0
  32. package/lib/infrastructure/services/SubscriptionService.d.ts.map +1 -0
  33. package/lib/infrastructure/services/SubscriptionService.js +110 -0
  34. package/lib/infrastructure/services/SubscriptionService.js.map +1 -0
  35. package/lib/presentation/hooks/useSubscription.d.ts +33 -0
  36. package/lib/presentation/hooks/useSubscription.d.ts.map +1 -0
  37. package/lib/presentation/hooks/useSubscription.js +129 -0
  38. package/lib/presentation/hooks/useSubscription.js.map +1 -0
  39. package/lib/utils/dateUtils.d.ts +39 -0
  40. package/lib/utils/dateUtils.d.ts.map +1 -0
  41. package/lib/utils/dateUtils.js +117 -0
  42. package/lib/utils/dateUtils.js.map +1 -0
  43. package/lib/utils/dateValidationUtils.d.ts +20 -0
  44. package/lib/utils/dateValidationUtils.d.ts.map +1 -0
  45. package/lib/utils/dateValidationUtils.js +39 -0
  46. package/lib/utils/dateValidationUtils.js.map +1 -0
  47. package/lib/utils/periodUtils.d.ts +38 -0
  48. package/lib/utils/periodUtils.d.ts.map +1 -0
  49. package/lib/utils/periodUtils.js +70 -0
  50. package/lib/utils/periodUtils.js.map +1 -0
  51. package/lib/utils/planDetectionUtils.d.ts +17 -0
  52. package/lib/utils/planDetectionUtils.d.ts.map +1 -0
  53. package/lib/utils/planDetectionUtils.js +31 -0
  54. package/lib/utils/planDetectionUtils.js.map +1 -0
  55. package/lib/utils/priceUtils.d.ts +23 -0
  56. package/lib/utils/priceUtils.d.ts.map +1 -0
  57. package/lib/utils/priceUtils.js +29 -0
  58. package/lib/utils/priceUtils.js.map +1 -0
  59. package/lib/utils/subscriptionConstants.d.ts +62 -0
  60. package/lib/utils/subscriptionConstants.d.ts.map +1 -0
  61. package/lib/utils/subscriptionConstants.js +61 -0
  62. package/lib/utils/subscriptionConstants.js.map +1 -0
  63. package/package.json +13 -3
  64. package/src/application/ports/ISubscriptionRepository.ts +0 -0
  65. package/src/application/ports/ISubscriptionService.ts +0 -0
  66. package/src/domain/entities/SubscriptionStatus.test.ts +106 -0
  67. package/src/domain/entities/SubscriptionStatus.ts +0 -0
  68. package/src/domain/errors/SubscriptionError.ts +0 -0
  69. package/src/domain/value-objects/SubscriptionConfig.ts +0 -0
  70. package/src/index.ts +9 -2
  71. package/src/infrastructure/services/ActivationHandler.ts +8 -0
  72. package/src/infrastructure/services/SubscriptionService.ts +13 -1
  73. package/src/presentation/hooks/useSubscription.ts +22 -2
  74. package/src/utils/dateUtils.test.ts +116 -0
  75. package/src/utils/dateUtils.ts +12 -76
  76. package/src/utils/dateValidationUtils.test.ts +142 -0
  77. package/src/utils/dateValidationUtils.ts +53 -0
  78. package/src/utils/periodUtils.ts +0 -0
  79. package/src/utils/planDetectionUtils.test.ts +47 -0
  80. package/src/utils/planDetectionUtils.ts +40 -0
  81. package/src/utils/priceUtils.test.ts +35 -0
  82. package/src/utils/priceUtils.ts +0 -0
  83. package/src/utils/subscriptionConstants.ts +0 -0
@@ -3,88 +3,20 @@
3
3
  * Subscription date-related helper functions
4
4
  *
5
5
  * Following SOLID, DRY, KISS principles:
6
- * - Single Responsibility: Only date-related operations
6
+ * - Single Responsibility: Only date formatting and calculation
7
7
  * - DRY: No code duplication
8
8
  * - KISS: Simple, clear implementations
9
9
  */
10
10
 
11
- import type { SubscriptionStatus } from '../domain/entities/SubscriptionStatus';
11
+ import { DATE_CONSTANTS } from './subscriptionConstants';
12
+ import { extractPlanFromProductId } from './planDetectionUtils';
12
13
  import {
13
14
  SUBSCRIPTION_PLAN_TYPES,
14
15
  MIN_SUBSCRIPTION_DURATIONS_DAYS,
15
16
  SUBSCRIPTION_PERIOD_DAYS,
16
- DATE_CONSTANTS,
17
- PRODUCT_ID_KEYWORDS,
18
17
  type SubscriptionPlanType,
19
18
  } from './subscriptionConstants';
20
19
 
21
- /**
22
- * Extract subscription plan type from product ID
23
- * Example: "com.umituz.app.weekly" → "weekly"
24
- * @internal
25
- */
26
- function extractPlanFromProductId(
27
- productId: string | null | undefined,
28
- ): SubscriptionPlanType {
29
- if (!productId) return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
30
-
31
- const lower = productId.toLowerCase();
32
-
33
- if (PRODUCT_ID_KEYWORDS.WEEKLY.some((keyword) => lower.includes(keyword))) {
34
- return SUBSCRIPTION_PLAN_TYPES.WEEKLY;
35
- }
36
- if (PRODUCT_ID_KEYWORDS.MONTHLY.some((keyword) => lower.includes(keyword))) {
37
- return SUBSCRIPTION_PLAN_TYPES.MONTHLY;
38
- }
39
- if (PRODUCT_ID_KEYWORDS.YEARLY.some((keyword) => lower.includes(keyword))) {
40
- return SUBSCRIPTION_PLAN_TYPES.YEARLY;
41
- }
42
-
43
- return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
44
- }
45
-
46
- /**
47
- * Check if subscription is expired
48
- */
49
- export function isSubscriptionExpired(
50
- status: SubscriptionStatus | null,
51
- ): boolean {
52
- if (!status || !status.isPremium) {
53
- return true;
54
- }
55
-
56
- if (!status.expiresAt) {
57
- // Lifetime subscription (no expiration)
58
- return false;
59
- }
60
-
61
- const expirationDate = new Date(status.expiresAt);
62
- const now = new Date();
63
-
64
- return expirationDate.getTime() <= now.getTime();
65
- }
66
-
67
- /**
68
- * Get days until subscription expires
69
- * Returns null for lifetime subscriptions
70
- */
71
- export function getDaysUntilExpiration(
72
- status: SubscriptionStatus | null,
73
- ): number | null {
74
- if (!status || !status.expiresAt) {
75
- return null;
76
- }
77
-
78
- const expirationDate = new Date(status.expiresAt);
79
- const now = new Date();
80
- const diffMs = expirationDate.getTime() - now.getTime();
81
- const diffDays = Math.ceil(
82
- diffMs / DATE_CONSTANTS.MILLISECONDS_PER_DAY,
83
- );
84
-
85
- return diffDays > 0 ? diffDays : 0;
86
- }
87
-
88
20
  /**
89
21
  * Format expiration date for display
90
22
  */
@@ -98,6 +30,9 @@ export function formatExpirationDate(
98
30
 
99
31
  try {
100
32
  const date = new Date(expiresAt);
33
+ if (isNaN(date.getTime())) {
34
+ return null;
35
+ }
101
36
  return date.toLocaleDateString(locale, {
102
37
  year: 'numeric',
103
38
  month: 'long',
@@ -118,24 +53,26 @@ export function formatExpirationDate(
118
53
  * - Yearly subscriptions: Same day next year (e.g., Nov 10, 2024 → Nov 10, 2025)
119
54
  * - Weekly subscriptions: +7 days
120
55
  *
121
- * @param productId - Product identifier (e.g., "com.umituz.app.monthly")
56
+ * @param productId - Product identifier (e.g., "com.company.app.monthly")
122
57
  * @param revenueCatExpiresAt - Optional expiration date from RevenueCat API
123
58
  * @returns ISO date string for expiration, or null if invalid
124
59
  *
125
60
  * @example
126
61
  * // Monthly subscription purchased on Nov 10, 2024
127
- * calculateExpirationDate('com.umituz.app.monthly', null)
62
+ * calculateExpirationDate('com.company.app.monthly', null)
128
63
  * // Returns: '2024-12-10T...' (Dec 10, 2024)
129
64
  *
130
65
  * @example
131
66
  * // Yearly subscription purchased on Nov 10, 2024
132
- * calculateExpirationDate('com.umituz.app.yearly', null)
67
+ * calculateExpirationDate('com.company.app.yearly', null)
133
68
  * // Returns: '2025-11-10T...' (Nov 10, 2025)
134
69
  */
135
70
  export function calculateExpirationDate(
136
71
  productId: string | null | undefined,
137
72
  revenueCatExpiresAt?: string | null,
138
73
  ): string | null {
74
+ if (!productId) return null;
75
+
139
76
  const plan = extractPlanFromProductId(productId);
140
77
  const now = new Date();
141
78
 
@@ -207,5 +144,4 @@ export function calculateExpirationDate(
207
144
  }
208
145
 
209
146
  return calculatedDate.toISOString();
210
- }
211
-
147
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tests for Date Validation Utilities
3
+ */
4
+
5
+ import {
6
+ isSubscriptionExpired,
7
+ getDaysUntilExpiration,
8
+ } from '../utils/dateValidationUtils';
9
+
10
+ describe('Date Validation Utils', () => {
11
+ describe('isSubscriptionExpired', () => {
12
+ it('should return true for null status', () => {
13
+ expect(isSubscriptionExpired(null)).toBe(true);
14
+ });
15
+
16
+ it('should return true for non-premium status', () => {
17
+ const status = {
18
+ isPremium: false,
19
+ expiresAt: null,
20
+ productId: null,
21
+ purchasedAt: null,
22
+ customerId: null,
23
+ syncedAt: null,
24
+ };
25
+
26
+ expect(isSubscriptionExpired(status)).toBe(true);
27
+ });
28
+
29
+ it('should return false for lifetime subscription', () => {
30
+ const status = {
31
+ isPremium: true,
32
+ expiresAt: null,
33
+ productId: 'lifetime',
34
+ purchasedAt: '2024-01-01T00:00:00.000Z',
35
+ customerId: 'customer123',
36
+ syncedAt: '2024-01-01T00:00:00.000Z',
37
+ };
38
+
39
+ expect(isSubscriptionExpired(status)).toBe(false);
40
+ });
41
+
42
+ it('should return false for future expiration', () => {
43
+ const futureDate = new Date();
44
+ futureDate.setDate(futureDate.getDate() + 30);
45
+
46
+ const status = {
47
+ isPremium: true,
48
+ expiresAt: futureDate.toISOString(),
49
+ productId: 'monthly',
50
+ purchasedAt: '2024-01-01T00:00:00.000Z',
51
+ customerId: 'customer123',
52
+ syncedAt: '2024-01-01T00:00:00.000Z',
53
+ };
54
+
55
+ expect(isSubscriptionExpired(status)).toBe(false);
56
+ });
57
+
58
+ it('should return true for past expiration', () => {
59
+ const pastDate = new Date();
60
+ pastDate.setDate(pastDate.getDate() - 1);
61
+
62
+ const status = {
63
+ isPremium: true,
64
+ expiresAt: pastDate.toISOString(),
65
+ productId: 'monthly',
66
+ purchasedAt: '2024-01-01T00:00:00.000Z',
67
+ customerId: 'customer123',
68
+ syncedAt: '2024-01-01T00:00:00.000Z',
69
+ };
70
+
71
+ const result = getDaysUntilExpiration(status);
72
+ expect(result === 0 || result === -0).toBe(true);
73
+ });
74
+ });
75
+
76
+ describe('getDaysUntilExpiration', () => {
77
+ it('should return null for null status', () => {
78
+ expect(getDaysUntilExpiration(null)).toBeNull();
79
+ });
80
+
81
+ it('should return null for status without expiration', () => {
82
+ const status = {
83
+ isPremium: true,
84
+ expiresAt: null,
85
+ productId: 'lifetime',
86
+ purchasedAt: '2024-01-01T00:00:00.000Z',
87
+ customerId: 'customer123',
88
+ syncedAt: '2024-01-01T00:00:00.000Z',
89
+ };
90
+
91
+ expect(getDaysUntilExpiration(status)).toBeNull();
92
+ });
93
+
94
+ it('should return positive days for future expiration', () => {
95
+ const futureDate = new Date();
96
+ futureDate.setDate(futureDate.getDate() + 5);
97
+
98
+ const status = {
99
+ isPremium: true,
100
+ expiresAt: futureDate.toISOString(),
101
+ productId: 'monthly',
102
+ purchasedAt: '2024-01-01T00:00:00.000Z',
103
+ customerId: 'customer123',
104
+ syncedAt: '2024-01-01T00:00:00.000Z',
105
+ };
106
+
107
+ expect(getDaysUntilExpiration(status)).toBe(5);
108
+ });
109
+
110
+ it('should return 0 for past expiration', () => {
111
+ const pastDate = new Date();
112
+ pastDate.setDate(pastDate.getDate() - 5);
113
+
114
+ const status = {
115
+ isPremium: true,
116
+ expiresAt: pastDate.toISOString(),
117
+ productId: 'monthly',
118
+ purchasedAt: '2024-01-01T00:00:00.000Z',
119
+ customerId: 'customer123',
120
+ syncedAt: '2024-01-01T00:00:00.000Z',
121
+ };
122
+
123
+ expect(getDaysUntilExpiration(status)).toBe(0);
124
+ });
125
+
126
+ it('should return 0 for today expiration', () => {
127
+ const today = new Date();
128
+ today.setHours(0, 0, 0, 0); // Start of today
129
+
130
+ const status = {
131
+ isPremium: true,
132
+ expiresAt: today.toISOString(),
133
+ productId: 'monthly',
134
+ purchasedAt: '2024-01-01T00:00:00.000Z',
135
+ customerId: 'customer123',
136
+ syncedAt: '2024-01-01T00:00:00.000Z',
137
+ };
138
+
139
+ expect(getDaysUntilExpiration(status)).toBe(0);
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Date Validation Utilities
3
+ * Utilities for validating and checking subscription dates
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only date validation logic
7
+ * - DRY: No code duplication
8
+ * - KISS: Simple, clear implementations
9
+ */
10
+
11
+ import type { SubscriptionStatus } from '../domain/entities/SubscriptionStatus';
12
+
13
+ /**
14
+ * Check if subscription is expired
15
+ */
16
+ export function isSubscriptionExpired(
17
+ status: SubscriptionStatus | null,
18
+ ): boolean {
19
+ if (!status || !status.isPremium) {
20
+ return true;
21
+ }
22
+
23
+ if (!status.expiresAt) {
24
+ // Lifetime subscription (no expiration)
25
+ return false;
26
+ }
27
+
28
+ const expirationDate = new Date(status.expiresAt);
29
+ const now = new Date();
30
+
31
+ return expirationDate.getTime() <= now.getTime();
32
+ }
33
+
34
+ /**
35
+ * Get days until subscription expires
36
+ * Returns null for lifetime subscriptions
37
+ */
38
+ export function getDaysUntilExpiration(
39
+ status: SubscriptionStatus | null,
40
+ ): number | null {
41
+ if (!status || !status.expiresAt) {
42
+ return null;
43
+ }
44
+
45
+ const expirationDate = new Date(status.expiresAt);
46
+ const now = new Date();
47
+ const diffMs = expirationDate.getTime() - now.getTime();
48
+ const diffDays = Math.ceil(
49
+ diffMs / (1000 * 60 * 60 * 24),
50
+ );
51
+
52
+ return Math.max(0, diffDays);
53
+ }
File without changes
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tests for Plan Detection Utilities
3
+ */
4
+
5
+ import { extractPlanFromProductId } from '../utils/planDetectionUtils';
6
+ import { SUBSCRIPTION_PLAN_TYPES } from '../utils/subscriptionConstants';
7
+
8
+ describe('Plan Detection Utils', () => {
9
+ describe('extractPlanFromProductId', () => {
10
+ it('should return UNKNOWN for null/undefined/empty productId', () => {
11
+ expect(extractPlanFromProductId(null)).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
12
+ expect(extractPlanFromProductId(undefined)).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
13
+ expect(extractPlanFromProductId('')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
14
+ });
15
+
16
+ it('should detect weekly plans', () => {
17
+ expect(extractPlanFromProductId('com.app.weekly')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
18
+ expect(extractPlanFromProductId('com.app.week')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
19
+ expect(extractPlanFromProductId('WEEKLY_PREMIUM')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
20
+ });
21
+
22
+ it('should detect monthly plans', () => {
23
+ expect(extractPlanFromProductId('com.app.monthly')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
24
+ expect(extractPlanFromProductId('com.app.month')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
25
+ expect(extractPlanFromProductId('MONTHLY_PREMIUM')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
26
+ });
27
+
28
+ it('should detect yearly plans', () => {
29
+ expect(extractPlanFromProductId('com.app.yearly')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
30
+ expect(extractPlanFromProductId('com.app.year')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
31
+ expect(extractPlanFromProductId('com.app.annual')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
32
+ expect(extractPlanFromProductId('YEARLY_PREMIUM')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
33
+ });
34
+
35
+ it('should be case insensitive', () => {
36
+ expect(extractPlanFromProductId('WEEKLY')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
37
+ expect(extractPlanFromProductId('Monthly')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
38
+ expect(extractPlanFromProductId('YEARLY')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
39
+ });
40
+
41
+ it('should return UNKNOWN for unrecognized patterns', () => {
42
+ expect(extractPlanFromProductId('com.app.lifetime')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
43
+ expect(extractPlanFromProductId('com.app.premium')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
44
+ expect(extractPlanFromProductId('random_string')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Plan Detection Utilities
3
+ * Utilities for detecting subscription plan types from product IDs
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only plan detection logic
7
+ * - DRY: No code duplication
8
+ * - KISS: Simple, clear implementations
9
+ */
10
+
11
+ import {
12
+ SUBSCRIPTION_PLAN_TYPES,
13
+ PRODUCT_ID_KEYWORDS,
14
+ type SubscriptionPlanType,
15
+ } from './subscriptionConstants';
16
+
17
+ /**
18
+ * Extract subscription plan type from product ID
19
+ * Example: "com.company.app.weekly" → "weekly"
20
+ * @internal
21
+ */
22
+ export function extractPlanFromProductId(
23
+ productId: string | null | undefined,
24
+ ): SubscriptionPlanType {
25
+ if (!productId) return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
26
+
27
+ const lower = productId.toLowerCase();
28
+
29
+ if (PRODUCT_ID_KEYWORDS.WEEKLY.some((keyword) => lower.includes(keyword))) {
30
+ return SUBSCRIPTION_PLAN_TYPES.WEEKLY;
31
+ }
32
+ if (PRODUCT_ID_KEYWORDS.MONTHLY.some((keyword) => lower.includes(keyword))) {
33
+ return SUBSCRIPTION_PLAN_TYPES.MONTHLY;
34
+ }
35
+ if (PRODUCT_ID_KEYWORDS.YEARLY.some((keyword) => lower.includes(keyword))) {
36
+ return SUBSCRIPTION_PLAN_TYPES.YEARLY;
37
+ }
38
+
39
+ return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
40
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Tests for Price Utilities
3
+ */
4
+
5
+ import { formatPrice } from '../utils/priceUtils';
6
+
7
+ describe('Price Utils', () => {
8
+ describe('formatPrice', () => {
9
+ it('should format USD price', () => {
10
+ expect(formatPrice(9.99, 'USD')).toBe('$9.99');
11
+ });
12
+
13
+ it('should format EUR price', () => {
14
+ expect(formatPrice(19.99, 'EUR')).toMatch(/€19\.99/);
15
+ });
16
+
17
+ it('should format TRY price', () => {
18
+ const result = formatPrice(229.99, 'TRY');
19
+ expect(result).toBeTruthy();
20
+ expect(result).toContain('229.99');
21
+ });
22
+
23
+ it('should format whole numbers', () => {
24
+ expect(formatPrice(10, 'USD')).toBe('$10.00');
25
+ });
26
+
27
+ it('should handle zero price', () => {
28
+ expect(formatPrice(0, 'USD')).toBe('$0.00');
29
+ });
30
+
31
+ it('should format large numbers', () => {
32
+ expect(formatPrice(999.99, 'USD')).toBe('$999.99');
33
+ });
34
+ });
35
+ });
File without changes
File without changes