@umituz/react-native-subscription 2.14.97 → 2.14.99
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/LICENSE +21 -0
- package/README.md +461 -0
- package/package.json +1 -3
- package/src/application/README.md +229 -0
- package/src/application/ports/README.md +103 -0
- package/src/domain/README.md +402 -0
- package/src/domain/constants/README.md +80 -0
- package/src/domain/entities/README.md +176 -0
- package/src/domain/errors/README.md +307 -0
- package/src/domain/value-objects/README.md +186 -0
- package/src/domains/README.md +240 -0
- package/src/domains/config/README.md +390 -0
- package/src/domains/config/domain/README.md +390 -0
- package/src/domains/config/domain/entities/README.md +350 -0
- package/src/domains/paywall/README.md +371 -0
- package/src/domains/paywall/components/PaywallHeader.tsx +8 -11
- package/src/domains/paywall/components/README.md +185 -0
- package/src/domains/paywall/entities/README.md +199 -0
- package/src/domains/paywall/hooks/README.md +129 -0
- package/src/domains/wallet/README.md +292 -0
- package/src/domains/wallet/domain/README.md +108 -0
- package/src/domains/wallet/domain/entities/README.md +122 -0
- package/src/domains/wallet/domain/errors/README.md +157 -0
- package/src/domains/wallet/infrastructure/README.md +96 -0
- package/src/domains/wallet/presentation/components/BalanceCard.tsx +6 -12
- package/src/domains/wallet/presentation/components/README.md +231 -0
- package/src/domains/wallet/presentation/hooks/README.md +255 -0
- package/src/infrastructure/README.md +514 -0
- package/src/infrastructure/mappers/README.md +34 -0
- package/src/infrastructure/models/README.md +26 -0
- package/src/infrastructure/repositories/README.md +385 -0
- package/src/infrastructure/services/README.md +374 -0
- package/src/presentation/README.md +410 -0
- package/src/presentation/components/README.md +183 -0
- package/src/presentation/components/details/CreditRow.md +337 -0
- package/src/presentation/components/details/DetailRow.md +283 -0
- package/src/presentation/components/details/PremiumDetailsCard.md +266 -0
- package/src/presentation/components/details/PremiumStatusBadge.md +266 -0
- package/src/presentation/components/details/README.md +449 -0
- package/src/presentation/components/feedback/PaywallFeedbackModal.md +314 -0
- package/src/presentation/components/feedback/README.md +447 -0
- package/src/presentation/components/paywall/PaywallModal.md +444 -0
- package/src/presentation/components/paywall/README.md +190 -0
- package/src/presentation/components/sections/README.md +468 -0
- package/src/presentation/components/sections/SubscriptionSection.md +246 -0
- package/src/presentation/hooks/README.md +743 -0
- package/src/presentation/hooks/useAuthAwarePurchase.md +359 -0
- package/src/presentation/hooks/useAuthGate.md +403 -0
- package/src/presentation/hooks/useAuthSubscriptionSync.md +398 -0
- package/src/presentation/hooks/useCreditChecker.md +407 -0
- package/src/presentation/hooks/useCredits.md +342 -0
- package/src/presentation/hooks/useCreditsGate.md +346 -0
- package/src/presentation/hooks/useDeductCredit.md +176 -0
- package/src/presentation/hooks/useDevTestCallbacks.md +422 -0
- package/src/presentation/hooks/useFeatureGate.md +157 -0
- package/src/presentation/hooks/useInitializeCredits.md +458 -0
- package/src/presentation/hooks/usePaywall.md +334 -0
- package/src/presentation/hooks/usePaywallOperations.md +486 -0
- package/src/presentation/hooks/usePaywallVisibility.md +344 -0
- package/src/presentation/hooks/usePremium.md +230 -0
- package/src/presentation/hooks/usePremiumGate.md +423 -0
- package/src/presentation/hooks/usePremiumWithCredits.md +429 -0
- package/src/presentation/hooks/useSubscription.md +450 -0
- package/src/presentation/hooks/useSubscriptionDetails.md +438 -0
- package/src/presentation/hooks/useSubscriptionGate.md +168 -0
- package/src/presentation/hooks/useSubscriptionSettingsConfig.md +374 -0
- package/src/presentation/hooks/useSubscriptionStatus.md +424 -0
- package/src/presentation/hooks/useUserTier.md +356 -0
- package/src/presentation/hooks/useUserTierWithRepository.md +452 -0
- package/src/presentation/screens/README.md +194 -0
- package/src/presentation/types/README.md +38 -0
- package/src/presentation/utils/README.md +52 -0
- package/src/revenuecat/README.md +523 -0
- package/src/revenuecat/application/README.md +158 -0
- package/src/revenuecat/application/ports/README.md +169 -0
- package/src/revenuecat/domain/README.md +147 -0
- package/src/revenuecat/domain/constants/README.md +183 -0
- package/src/revenuecat/domain/entities/README.md +382 -0
- package/src/revenuecat/domain/errors/README.md +197 -0
- package/src/revenuecat/domain/types/README.md +373 -0
- package/src/revenuecat/domain/value-objects/README.md +441 -0
- package/src/revenuecat/infrastructure/README.md +50 -0
- package/src/revenuecat/infrastructure/config/README.md +40 -0
- package/src/revenuecat/infrastructure/handlers/README.md +218 -0
- package/src/revenuecat/infrastructure/managers/README.md +49 -0
- package/src/revenuecat/infrastructure/services/README.md +325 -0
- package/src/revenuecat/infrastructure/utils/README.md +382 -0
- package/src/revenuecat/presentation/README.md +184 -0
- package/src/revenuecat/presentation/hooks/README.md +56 -0
- package/src/utils/README.md +529 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Domain Layer
|
|
2
|
+
|
|
3
|
+
Abonelik sisteminin temel domain logic'ini, entity'lerini ve value object'lerini içeren katman.
|
|
4
|
+
|
|
5
|
+
## Sorumluluklar
|
|
6
|
+
|
|
7
|
+
- **Entities**: Domain entity'lerini tanımlama
|
|
8
|
+
- **Value Objects**: Değer objelerini oluşturma
|
|
9
|
+
- **Domain Errors**: Domain-specific hataları tanımlama
|
|
10
|
+
- **Business Rules**: İş kurallarını encapsulate etme
|
|
11
|
+
|
|
12
|
+
## Yapı
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
domain/
|
|
16
|
+
├── entities/
|
|
17
|
+
│ └── SubscriptionStatus.ts # Abonelik durumu entity'si
|
|
18
|
+
├── value-objects/
|
|
19
|
+
│ └── SubscriptionConfig.ts # Konfigürasyon value object
|
|
20
|
+
└── errors/
|
|
21
|
+
├── SubscriptionError.ts # Abonelik hataları
|
|
22
|
+
└── InsufficientCreditsError.ts # Kredi yetersizlik hatası
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Entities
|
|
26
|
+
|
|
27
|
+
### SubscriptionStatus
|
|
28
|
+
|
|
29
|
+
Kullanıcının abonelik durumunu temsil eden ana entity:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
SubscriptionStatus,
|
|
34
|
+
SubscriptionStatusType,
|
|
35
|
+
createDefaultSubscriptionStatus,
|
|
36
|
+
isSubscriptionValid,
|
|
37
|
+
} from '@umituz/react-native-subscription';
|
|
38
|
+
|
|
39
|
+
// Varsayılan durum oluştur
|
|
40
|
+
const defaultStatus = createDefaultSubscriptionStatus();
|
|
41
|
+
// {
|
|
42
|
+
// type: 'unknown',
|
|
43
|
+
// isActive: false,
|
|
44
|
+
// isPremium: false,
|
|
45
|
+
// expirationDate: null,
|
|
46
|
+
// willRenew: false,
|
|
47
|
+
// }
|
|
48
|
+
|
|
49
|
+
// Premium durumu oluştur
|
|
50
|
+
const premiumStatus: SubscriptionStatus = {
|
|
51
|
+
type: 'premium',
|
|
52
|
+
isActive: true,
|
|
53
|
+
isPremium: true,
|
|
54
|
+
expirationDate: '2025-12-31T23:59:59Z',
|
|
55
|
+
willRenew: true,
|
|
56
|
+
productId: 'com.app.premium.annual',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Abonelik geçerliliğini kontrol et
|
|
60
|
+
const isValid = isSubscriptionValid(premiumStatus); // true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**SubscriptionStatusType:**
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
type SubscriptionStatusType =
|
|
67
|
+
| 'unknown' // Durum bilinmiyor
|
|
68
|
+
| 'guest' // Misafir kullanıcı
|
|
69
|
+
| 'free' // Free kullanıcı
|
|
70
|
+
| 'premium'; // Premium kullanıcı
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Value Objects
|
|
74
|
+
|
|
75
|
+
### SubscriptionConfig
|
|
76
|
+
|
|
77
|
+
Abonelik konfigürasyonu için value object:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import type { SubscriptionConfig } from '@umituz/react-native-subscription';
|
|
81
|
+
|
|
82
|
+
const config: SubscriptionConfig = {
|
|
83
|
+
revenueCatApiKey: 'your_api_key',
|
|
84
|
+
revenueCatEntitlementId: 'premium',
|
|
85
|
+
|
|
86
|
+
plans: {
|
|
87
|
+
monthly: monthlyPlan,
|
|
88
|
+
annual: annualPlan,
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
defaultPlan: 'monthly',
|
|
92
|
+
|
|
93
|
+
features: {
|
|
94
|
+
requireAuth: true,
|
|
95
|
+
allowRestore: true,
|
|
96
|
+
syncWithFirebase: true,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
ui: {
|
|
100
|
+
showAnnualDiscount: true,
|
|
101
|
+
highlightPopularPlan: true,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Optional callbacks
|
|
105
|
+
onStatusChanged: (userId, newStatus) => {
|
|
106
|
+
console.log(`Status changed for ${userId}:`, newStatus);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
onError: (error) => {
|
|
110
|
+
console.error('Subscription error:', error);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Domain Errors
|
|
116
|
+
|
|
117
|
+
### SubscriptionError
|
|
118
|
+
|
|
119
|
+
Abonelik işlemleri için hata sınıfı:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import {
|
|
123
|
+
SubscriptionError,
|
|
124
|
+
SubscriptionValidationError,
|
|
125
|
+
SubscriptionRepositoryError,
|
|
126
|
+
} from '@umituz/react-native-subscription';
|
|
127
|
+
|
|
128
|
+
// Doğrulama hatası
|
|
129
|
+
throw new SubscriptionValidationError('Invalid user ID');
|
|
130
|
+
|
|
131
|
+
// Repository hatası
|
|
132
|
+
throw new SubscriptionRepositoryError('Database connection failed');
|
|
133
|
+
|
|
134
|
+
// Genel hata
|
|
135
|
+
throw new SubscriptionError('Subscription operation failed');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### InsufficientCreditsError
|
|
139
|
+
|
|
140
|
+
Kredi yetersizliği hatası:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import {
|
|
144
|
+
InsufficientCreditsError,
|
|
145
|
+
type CreditErrorContext,
|
|
146
|
+
} from '@umituz/react-native-subscription';
|
|
147
|
+
|
|
148
|
+
const context: CreditErrorContext = {
|
|
149
|
+
required: 10,
|
|
150
|
+
available: 5,
|
|
151
|
+
featureId: 'ai_generation',
|
|
152
|
+
currency: 'USD',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
throw new InsufficientCreditsError(
|
|
156
|
+
'Not enough credits',
|
|
157
|
+
context
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Hata yakalama
|
|
161
|
+
try {
|
|
162
|
+
await deductCredits(userId, 10);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof InsufficientCreditsError) {
|
|
165
|
+
console.log(`Required: ${error.context.required}`);
|
|
166
|
+
console.log(`Available: ${error.context.available}`);
|
|
167
|
+
// Paywall göster
|
|
168
|
+
showPaywall();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Helper Functions
|
|
174
|
+
|
|
175
|
+
### SubscriptionStatus Helpers
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import {
|
|
179
|
+
createDefaultSubscriptionStatus,
|
|
180
|
+
isSubscriptionValid,
|
|
181
|
+
isSubscriptionActive,
|
|
182
|
+
isUserPremium,
|
|
183
|
+
getSubscriptionTier,
|
|
184
|
+
} from '@umituz/react-native-subscription';
|
|
185
|
+
|
|
186
|
+
// Varsayılan durum oluştur
|
|
187
|
+
const status = createDefaultSubscriptionStatus();
|
|
188
|
+
|
|
189
|
+
// Geçerlilik kontrolü
|
|
190
|
+
const isValid = isSubscriptionValid(status);
|
|
191
|
+
|
|
192
|
+
// Aktif kontrolü
|
|
193
|
+
const isActive = isSubscriptionActive(status);
|
|
194
|
+
|
|
195
|
+
// Premium kontrolü
|
|
196
|
+
const isPremium = isUserPremium(status);
|
|
197
|
+
|
|
198
|
+
// Tier bilgisi
|
|
199
|
+
const tier = getSubscriptionTier(status); // 'free', 'premium'
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Factory Functions
|
|
203
|
+
|
|
204
|
+
### Entity Factory
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { SubscriptionStatus } from '@umituz/react-native-subscription';
|
|
208
|
+
|
|
209
|
+
// Factory method ile oluştur
|
|
210
|
+
const status = SubscriptionStatus.create({
|
|
211
|
+
type: 'premium',
|
|
212
|
+
isActive: true,
|
|
213
|
+
isPremium: true,
|
|
214
|
+
expirationDate: '2025-12-31T23:59:59Z',
|
|
215
|
+
willRenew: true,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Validation içerir
|
|
219
|
+
try {
|
|
220
|
+
const invalid = SubscriptionStatus.create({
|
|
221
|
+
type: 'premium',
|
|
222
|
+
isActive: false, // Çelişki: premium ama aktif değil
|
|
223
|
+
isPremium: true,
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('Validation error:', error.message);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Type Guards
|
|
231
|
+
|
|
232
|
+
Domain entities için type guard'lar:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import {
|
|
236
|
+
isSubscriptionStatus,
|
|
237
|
+
isValidSubscriptionStatus,
|
|
238
|
+
} from '@umituz/react-native-subscription';
|
|
239
|
+
|
|
240
|
+
// Type guard
|
|
241
|
+
const obj = { type: 'premium', isActive: true, isPremium: true };
|
|
242
|
+
|
|
243
|
+
if (isSubscriptionStatus(obj)) {
|
|
244
|
+
// obj artık SubscriptionStatus tipinde
|
|
245
|
+
console.log(obj.type); // Type-safe!
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Validasyon + type guard
|
|
249
|
+
if (isValidSubscriptionStatus(obj)) {
|
|
250
|
+
// Geçerli bir SubscriptionStatus
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Domain Rules
|
|
255
|
+
|
|
256
|
+
### Business Rule Examples
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { SubscriptionStatus } from '@umituz/react-native-subscription';
|
|
260
|
+
|
|
261
|
+
class SubscriptionDomain {
|
|
262
|
+
// Kural: Premium kullanıcı her zaman aktif olmalı
|
|
263
|
+
static validatePremiumStatus(status: SubscriptionStatus): boolean {
|
|
264
|
+
if (status.isPremium && !status.isActive) {
|
|
265
|
+
throw new Error('Premium users must be active');
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Kural: Abonelik tarihi geçmişse aktif olmamalı
|
|
271
|
+
static validateExpiration(status: SubscriptionStatus): boolean {
|
|
272
|
+
if (status.expirationDate) {
|
|
273
|
+
const isExpired = new Date(status.expirationDate) < new Date();
|
|
274
|
+
if (isExpired && status.isActive) {
|
|
275
|
+
throw new Error('Expired subscription cannot be active');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Kural: Lifetime aboneliklerin son kullanma tarihi olmamalı
|
|
282
|
+
static validateLifetime(status: SubscriptionStatus): boolean {
|
|
283
|
+
if (status.type === 'premium' && status.willRenew === false) {
|
|
284
|
+
if (status.expirationDate !== null) {
|
|
285
|
+
throw new Error('Lifetime subscriptions cannot have expiration date');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Best Practices
|
|
294
|
+
|
|
295
|
+
1. **Immutable Objects**: Entity'leri immutable olarak tasarlayın
|
|
296
|
+
2. **Validation**: Entity creation'da validation yapın
|
|
297
|
+
3. **Encapsulation**: Business logic'i entity içinde tutun
|
|
298
|
+
4. **Type Safety**: Strong typing kullanın
|
|
299
|
+
5. **Domain Events**: Önemli domain olaylarını event olarak yayınlayın
|
|
300
|
+
6. **Error Handling**: Domain-specific hatalar tanımlayın
|
|
301
|
+
|
|
302
|
+
## Örnek: Domain Service
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import {
|
|
306
|
+
SubscriptionStatus,
|
|
307
|
+
SubscriptionError,
|
|
308
|
+
isSubscriptionValid,
|
|
309
|
+
} from '@umituz/react-native-subscription';
|
|
310
|
+
|
|
311
|
+
class SubscriptionDomainService {
|
|
312
|
+
// Abonelik aktifleştirme
|
|
313
|
+
activateSubscription(
|
|
314
|
+
currentStatus: SubscriptionStatus,
|
|
315
|
+
productId: string,
|
|
316
|
+
expiresAt: string
|
|
317
|
+
): SubscriptionStatus {
|
|
318
|
+
// Business rule: Zaten aktifse hata
|
|
319
|
+
if (currentStatus.isActive) {
|
|
320
|
+
throw new SubscriptionError('Subscription is already active');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Yeni durum oluştur
|
|
324
|
+
const newStatus: SubscriptionStatus = {
|
|
325
|
+
type: 'premium',
|
|
326
|
+
isActive: true,
|
|
327
|
+
isPremium: true,
|
|
328
|
+
expirationDate: expiresAt,
|
|
329
|
+
willRenew: productId.includes('monthly') || productId.includes('annual'),
|
|
330
|
+
productId,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return newStatus;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Abonelik iptali
|
|
337
|
+
deactivateSubscription(
|
|
338
|
+
status: SubscriptionStatus
|
|
339
|
+
): SubscriptionStatus {
|
|
340
|
+
if (!status.isActive) {
|
|
341
|
+
throw new SubscriptionError('Subscription is not active');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
...status,
|
|
346
|
+
isActive: false,
|
|
347
|
+
willRenew: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Abonelik yenileme
|
|
352
|
+
renewSubscription(
|
|
353
|
+
status: SubscriptionStatus,
|
|
354
|
+
newExpirationDate: string
|
|
355
|
+
): SubscriptionStatus {
|
|
356
|
+
if (!status.willRenew) {
|
|
357
|
+
throw new SubscriptionError('Subscription is set to not renew');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
...status,
|
|
362
|
+
expirationDate: newExpirationDate,
|
|
363
|
+
isActive: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Testing
|
|
370
|
+
|
|
371
|
+
Domain entities test edilebilir olmalı:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { SubscriptionStatus, isSubscriptionValid } from '@umituz/react-native-subscription';
|
|
375
|
+
|
|
376
|
+
describe('SubscriptionStatus', () => {
|
|
377
|
+
it('should create valid premium status', () => {
|
|
378
|
+
const status = SubscriptionStatus.create({
|
|
379
|
+
type: 'premium',
|
|
380
|
+
isActive: true,
|
|
381
|
+
isPremium: true,
|
|
382
|
+
expirationDate: '2025-12-31T23:59:59Z',
|
|
383
|
+
willRenew: true,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
expect(status.type).toBe('premium');
|
|
387
|
+
expect(status.isPremium).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should validate expiration', () => {
|
|
391
|
+
const expired = SubscriptionStatus.create({
|
|
392
|
+
type: 'premium',
|
|
393
|
+
isActive: true,
|
|
394
|
+
isPremium: true,
|
|
395
|
+
expirationDate: '2020-01-01T00:00:00Z',
|
|
396
|
+
willRenew: true,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(isSubscriptionValid(expired)).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Domain Constants
|
|
2
|
+
|
|
3
|
+
Constants used throughout the domain layer.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This directory contains constant definitions for subscription tiers, package periods, error codes, and other domain values.
|
|
8
|
+
|
|
9
|
+
## Constants
|
|
10
|
+
|
|
11
|
+
### Subscription Tiers
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
export const SUBSCRIPTION_TIERS = {
|
|
15
|
+
GUEST: 'guest',
|
|
16
|
+
FREE: 'free',
|
|
17
|
+
PREMIUM: 'premium',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type SubscriptionTier = typeof SUBSCRIPTION_TIERS[keyof typeof SUBSCRIPTION_TIERS];
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Package Periods
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export const PACKAGE_PERIODS = {
|
|
27
|
+
MONTHLY: 'monthly',
|
|
28
|
+
ANNUAL: 'annual',
|
|
29
|
+
LIFETIME: 'lifetime',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type PackagePeriod = typeof PACKAGE_PERIODS[keyof typeof PACKAGE_PERIODS];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Error Codes
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
export const ERROR_CODES = {
|
|
39
|
+
CREDITS_EXHAUSTED: 'CREDITS_EXHAUSTED',
|
|
40
|
+
USER_NOT_AUTHENTICATED: 'USER_NOT_AUTHENTICATED',
|
|
41
|
+
SUBSCRIPTION_EXPIRED: 'SUBSCRIPTION_EXPIRED',
|
|
42
|
+
INVALID_PACKAGE: 'INVALID_PACKAGE',
|
|
43
|
+
PURCHASE_FAILED: 'PURCHASE_FAILED',
|
|
44
|
+
DUPLICATE_PURCHASE: 'DUPLICATE_PURCHASE',
|
|
45
|
+
} as const;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Credit Limits
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
export const CREDIT_LIMITS = {
|
|
52
|
+
MAX_MONTHLY_CREDITS: 100,
|
|
53
|
+
MAX_BONUS_CREDITS: 1000,
|
|
54
|
+
MIN_CREDITS: 0,
|
|
55
|
+
} as const;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Time Periods
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export const TIME_PERIODS = {
|
|
62
|
+
CREDIT_RESET_DAYS: 30,
|
|
63
|
+
EXPIRATION_WARNING_DAYS: 7,
|
|
64
|
+
EXPIRATION_CRITICAL_DAYS: 3,
|
|
65
|
+
} as const;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { SUBSCRIPTION_TIERS, PACKAGE_PERIODS } from './constants';
|
|
72
|
+
|
|
73
|
+
const tier: SubscriptionTier = SUBSCRIPTION_TIERS.PREMIUM;
|
|
74
|
+
const period: PackagePeriod = PACKAGE_PERIODS.ANNUAL;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Related
|
|
78
|
+
|
|
79
|
+
- [Domain README](../README.md)
|
|
80
|
+
- [Entities](../entities/README.md)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Domain Entities
|
|
2
|
+
|
|
3
|
+
Core domain entities for subscription management.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Domain entities represent the core business concepts and rules of the subscription system. They are framework-agnostic and contain only business logic.
|
|
8
|
+
|
|
9
|
+
## Entities
|
|
10
|
+
|
|
11
|
+
### SubscriptionStatus
|
|
12
|
+
|
|
13
|
+
Represents the subscription state of a user.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
interface SubscriptionStatus {
|
|
17
|
+
type: SubscriptionStatusType;
|
|
18
|
+
isActive: boolean;
|
|
19
|
+
isPremium: boolean;
|
|
20
|
+
expirationDate: string | null;
|
|
21
|
+
willRenew: boolean;
|
|
22
|
+
productId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type SubscriptionStatusType =
|
|
26
|
+
| 'unknown'
|
|
27
|
+
| 'guest'
|
|
28
|
+
| 'free'
|
|
29
|
+
| 'premium';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Usage:**
|
|
33
|
+
```typescript
|
|
34
|
+
import { SubscriptionStatus } from '@umituz/react-native-subscription/domain';
|
|
35
|
+
|
|
36
|
+
const status = SubscriptionStatus.create({
|
|
37
|
+
type: 'premium',
|
|
38
|
+
isActive: true,
|
|
39
|
+
isPremium: true,
|
|
40
|
+
expirationDate: '2025-12-31',
|
|
41
|
+
willRenew: true,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Design Principles
|
|
46
|
+
|
|
47
|
+
### 1. Self-Validation
|
|
48
|
+
|
|
49
|
+
Entities validate themselves on creation:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
class SubscriptionStatus {
|
|
53
|
+
private constructor(data: SubscriptionStatusData) {
|
|
54
|
+
this.validate(data);
|
|
55
|
+
// ...
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private validate(data: SubscriptionStatusData): void {
|
|
59
|
+
if (data.isPremium && !data.isActive) {
|
|
60
|
+
throw new ValidationError('Premium users must be active');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Immutable State
|
|
67
|
+
|
|
68
|
+
Entities cannot be modified after creation:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const status = SubscriptionStatus.create({...});
|
|
72
|
+
// status.isActive = false; // Error: Cannot assign to read-only property
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Business Rules
|
|
76
|
+
|
|
77
|
+
Business logic is encapsulated in entities:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
class SubscriptionStatus {
|
|
81
|
+
isExpired(): boolean {
|
|
82
|
+
if (!this.expirationDate) return false;
|
|
83
|
+
return new Date(this.expirationDate) < new Date();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
requiresRenewal(): boolean {
|
|
87
|
+
return this.isPremium && this.expirationDate && this.willRenew;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Creating Entities
|
|
93
|
+
|
|
94
|
+
### Factory Method
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const status = SubscriptionStatus.create({
|
|
98
|
+
type: 'premium',
|
|
99
|
+
isActive: true,
|
|
100
|
+
isPremium: true,
|
|
101
|
+
expirationDate: null,
|
|
102
|
+
willRenew: false,
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Validation
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
try {
|
|
110
|
+
const status = SubscriptionStatus.create({
|
|
111
|
+
type: 'premium',
|
|
112
|
+
isActive: false, // Invalid!
|
|
113
|
+
isPremium: true,
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Validation failed:', error.message);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Domain Services
|
|
121
|
+
|
|
122
|
+
Business operations that don't naturally fit in entities:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
class SubscriptionDomainService {
|
|
126
|
+
canUpgrade(currentStatus: SubscriptionStatus): boolean {
|
|
127
|
+
return !currentStatus.isPremium;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
calculateDaysUntilExpiry(status: SubscriptionStatus): number | null {
|
|
131
|
+
if (!status.expirationDate) return null;
|
|
132
|
+
const diff = new Date(status.expirationDate).getTime() - Date.now();
|
|
133
|
+
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Best Practices
|
|
139
|
+
|
|
140
|
+
1. **Keep entities pure** - No framework dependencies
|
|
141
|
+
2. **Validate invariants** - Ensure valid state
|
|
142
|
+
3. **Use value objects** - For complex attributes
|
|
143
|
+
4. **Encapsulate logic** - Keep business rules inside entities
|
|
144
|
+
5. **Make immutable** - Prevent direct state modification
|
|
145
|
+
|
|
146
|
+
## Testing
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
describe('SubscriptionStatus', () => {
|
|
150
|
+
it('should create valid premium status', () => {
|
|
151
|
+
const status = SubscriptionStatus.create({
|
|
152
|
+
type: 'premium',
|
|
153
|
+
isActive: true,
|
|
154
|
+
isPremium: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(status.isPremium).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should reject invalid premium status', () => {
|
|
161
|
+
expect(() => {
|
|
162
|
+
SubscriptionStatus.create({
|
|
163
|
+
type: 'premium',
|
|
164
|
+
isActive: false,
|
|
165
|
+
isPremium: true,
|
|
166
|
+
});
|
|
167
|
+
}).toThrow();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Related
|
|
173
|
+
|
|
174
|
+
- [Value Objects](../value-objects/README.md)
|
|
175
|
+
- [Domain Errors](../errors/README.md)
|
|
176
|
+
- [Domain Layer](../../README.md)
|