@umituz/react-native-subscription 2.27.126 → 2.27.133

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.126",
3
+ "version": "2.27.133",
4
4
  "description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -2,16 +2,16 @@ import type { CreditsConfig } from "../core/Credits";
2
2
  import { detectPackageType } from "../../../utils/packageTypeDetector";
3
3
  import { getCreditAllocation } from "../../../utils/creditMapper";
4
4
 
5
- export class CreditLimitCalculator {
6
- static calculate(productId: string | undefined, config: CreditsConfig): number {
7
- if (!productId) return config.creditLimit;
5
+ export function calculateCreditLimit(productId: string | undefined, config: CreditsConfig): number {
6
+ if (!productId) return config.creditLimit;
8
7
 
9
- const explicitAmount = config.creditPackageAmounts?.[productId];
10
- if (explicitAmount) return explicitAmount;
8
+ const explicitAmount = config.creditPackageAmounts?.[productId];
9
+ if (explicitAmount) return explicitAmount;
11
10
 
12
- const packageType = detectPackageType(productId);
13
- const dynamicLimit = getCreditAllocation(packageType, config.packageAllocations);
14
-
15
- return dynamicLimit ?? config.creditLimit;
16
- }
11
+ const packageType = detectPackageType(productId);
12
+ const dynamicLimit = getCreditAllocation(packageType, config.packageAllocations);
13
+
14
+ return dynamicLimit ?? config.creditLimit;
17
15
  }
16
+
17
+
@@ -1,14 +1,12 @@
1
1
  import type { CreditsConfig } from "../core/Credits";
2
- import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
3
2
  import { getAppVersion, validatePlatform } from "../../../utils/appUtils";
4
3
 
5
4
  import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
6
- import { runTransaction, type Transaction, type DocumentReference } from "firebase/firestore";
7
- import type { Firestore } from "firebase/firestore";
5
+ import { runTransaction, type Transaction, type DocumentReference, type Firestore } from "firebase/firestore";
8
6
  import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
9
7
  import { calculateNewCredits, buildCreditsData, shouldSkipStatusSyncWrite } from "./creditOperationUtils";
10
- import { CreditLimitCalculator } from "./CreditLimitCalculator";
11
- import { PurchaseMetadataGenerator } from "./PurchaseMetadataGenerator";
8
+ import { calculateCreditLimit } from "./CreditLimitCalculator";
9
+ import { generatePurchaseMetadata } from "./PurchaseMetadataGenerator";
12
10
 
13
11
  export async function initializeCreditsTransaction(
14
12
  db: Firestore,
@@ -17,14 +15,13 @@ export async function initializeCreditsTransaction(
17
15
  purchaseId: string,
18
16
  metadata: InitializeCreditsMetadata
19
17
  ): Promise<InitializationResult> {
20
- if (!db) {
21
- throw new Error("Firestore instance is not available");
22
- }
18
+ if (!db) throw new Error("Firestore instance is not available");
19
+
20
+ const platform = validatePlatform();
21
+ const appVersion = getAppVersion();
23
22
 
24
23
  return runTransaction(db, async (transaction: Transaction) => {
25
24
  const creditsDoc = await transaction.get(creditsRef);
26
- const platform = validatePlatform();
27
-
28
25
  const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
29
26
 
30
27
  if (existingData.processedPurchases.includes(purchaseId)) {
@@ -35,10 +32,8 @@ export async function initializeCreditsTransaction(
35
32
  };
36
33
  }
37
34
 
38
- const creditLimit = CreditLimitCalculator.calculate(metadata.productId, config);
39
- const appVersion = getAppVersion();
40
-
41
- const { purchaseHistory } = PurchaseMetadataGenerator.generate({
35
+ const creditLimit = calculateCreditLimit(metadata.productId, config);
36
+ const { purchaseHistory } = generatePurchaseMetadata({
42
37
  productId: metadata.productId,
43
38
  source: metadata.source,
44
39
  type: metadata.type,
@@ -74,15 +69,10 @@ export async function initializeCreditsTransaction(
74
69
 
75
70
  transaction.set(creditsRef, creditsData, { merge: true });
76
71
 
77
- const finalData: UserCreditsDocumentRead = {
78
- ...existingData,
79
- ...creditsData,
80
- };
81
-
82
72
  return {
83
73
  credits: newCredits,
84
74
  alreadyProcessed: false,
85
- finalData
75
+ finalData: { ...existingData, ...creditsData }
86
76
  };
87
77
  });
88
78
  }
@@ -1,70 +1,57 @@
1
- import { runTransaction, serverTimestamp, type Firestore, type Transaction, type DocumentReference } from "firebase/firestore";
2
- import { getFirestore } from "@umituz/react-native-firebase";
1
+ import { runTransaction, serverTimestamp, type Transaction, type DocumentReference, type Firestore } from "firebase/firestore";
3
2
  import type { DeductCreditsResult } from "../core/Credits";
3
+ import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
5
 
6
- export interface IDeductCreditsCommand {
7
- execute(userId: string, cost: number): Promise<DeductCreditsResult>;
8
- }
9
-
10
6
  /**
11
- * Command for deducting credits.
7
+ * Deducts credits from a user's balance.
12
8
  * Encapsulates the domain rules and transaction logic for credit usage.
13
9
  */
14
- export class DeductCreditsCommand implements IDeductCreditsCommand {
15
- constructor(
16
- private getCreditsRef: (db: Firestore, userId: string) => DocumentReference
17
- ) {}
18
-
19
- async execute(userId: string, cost: number): Promise<DeductCreditsResult> {
20
- const db = getFirestore();
21
- if (!db) {
22
- return {
23
- success: false,
24
- remainingCredits: null,
25
- error: { message: "No DB", code: "ERR" }
26
- };
27
- }
28
-
29
- try {
30
- const remaining = await runTransaction(db, async (tx: Transaction) => {
31
- const ref = this.getCreditsRef(db, userId);
32
- const docSnap = await tx.get(ref);
33
-
34
- if (!docSnap.exists()) {
35
- throw new Error("NO_CREDITS");
36
- }
37
-
38
- const current = docSnap.data().credits as number;
39
- if (current < cost) {
40
- throw new Error("CREDITS_EXHAUSTED");
41
- }
42
-
43
- const updated = current - cost;
44
- tx.update(ref, {
45
- credits: updated,
46
- lastUpdatedAt: serverTimestamp()
47
- });
48
-
49
- return updated;
10
+ export async function deductCreditsOperation(
11
+ db: Firestore,
12
+ creditsRef: DocumentReference,
13
+ cost: number,
14
+ userId: string
15
+ ): Promise<DeductCreditsResult> {
16
+ try {
17
+ const remaining = await runTransaction(db, async (tx: Transaction) => {
18
+ const docSnap = await tx.get(creditsRef);
19
+
20
+ if (!docSnap.exists()) {
21
+ throw new Error(CREDIT_ERROR_CODES.NO_CREDITS);
22
+ }
23
+
24
+ const current = docSnap.data().credits as number;
25
+ if (current < cost) {
26
+ throw new Error(CREDIT_ERROR_CODES.CREDITS_EXHAUSTED);
27
+ }
28
+
29
+ const updated = current - cost;
30
+ tx.update(creditsRef, {
31
+ credits: updated,
32
+ lastUpdatedAt: serverTimestamp()
50
33
  });
51
34
 
52
- // Emit event via EventBus (Observer Pattern)
53
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
54
-
55
- return {
56
- success: true,
57
- remainingCredits: remaining,
58
- error: null
59
- };
60
- } catch (e: unknown) {
61
- const message = e instanceof Error ? e.message : String(e);
62
- const code = message === "NO_CREDITS" || message === "CREDITS_EXHAUSTED" ? message : "DEDUCT_ERR";
63
- return {
64
- success: false,
65
- remainingCredits: null,
66
- error: { message, code }
67
- };
68
- }
35
+ return updated;
36
+ });
37
+
38
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
39
+
40
+ return {
41
+ success: true,
42
+ remainingCredits: remaining,
43
+ error: null
44
+ };
45
+ } catch (e: unknown) {
46
+ const message = e instanceof Error ? e.message : String(e);
47
+ const code = (message === CREDIT_ERROR_CODES.NO_CREDITS || message === CREDIT_ERROR_CODES.CREDITS_EXHAUSTED)
48
+ ? message
49
+ : CREDIT_ERROR_CODES.DEDUCT_ERR;
50
+
51
+ return {
52
+ success: false,
53
+ remainingCredits: null,
54
+ error: { message, code }
55
+ };
69
56
  }
70
57
  }
@@ -6,48 +6,42 @@ import type {
6
6
  PurchaseSource
7
7
  } from "../core/UserCreditsDocument";
8
8
  import { detectPackageType } from "../../../utils/packageTypeDetector";
9
+ import { PACKAGE_TYPE, PURCHASE_TYPE, type Platform } from "../../subscription/core/SubscriptionConstants";
9
10
 
10
11
  export interface MetadataGeneratorConfig {
11
12
  productId: string;
12
13
  source: PurchaseSource;
13
14
  type: PurchaseType;
14
15
  creditLimit: number;
15
- platform: "ios" | "android";
16
+ platform: Platform;
16
17
  appVersion: string;
17
18
  }
18
19
 
19
- export class PurchaseMetadataGenerator {
20
- static generate(
21
- config: MetadataGeneratorConfig,
22
- existingData: UserCreditsDocumentRead
23
- ): { purchaseType: PurchaseType; purchaseHistory: PurchaseMetadata[] } {
24
- const { productId, source, type, creditLimit, platform, appVersion } = config;
20
+ export function generatePurchaseMetadata(
21
+ config: MetadataGeneratorConfig,
22
+ existingData: UserCreditsDocumentRead
23
+ ): { purchaseType: PurchaseType; purchaseHistory: PurchaseMetadata[] } {
24
+ const { productId, source, type, creditLimit, platform, appVersion } = config;
25
25
 
26
- const packageType = detectPackageType(productId);
27
- let purchaseType: PurchaseType = type;
26
+ const packageType = detectPackageType(productId);
27
+ let purchaseType: PurchaseType = type;
28
28
 
29
- if (packageType !== "unknown") {
30
- const oldLimit = existingData.creditLimit;
31
- if (creditLimit > oldLimit) {
32
- purchaseType = "upgrade";
33
- } else if (creditLimit < oldLimit) {
34
- purchaseType = "downgrade";
35
- }
36
- }
29
+ if (packageType !== PACKAGE_TYPE.UNKNOWN && creditLimit > existingData.creditLimit) {
30
+ purchaseType = PURCHASE_TYPE.UPGRADE;
31
+ }
37
32
 
38
- const newMetadata: PurchaseMetadata = {
39
- productId,
40
- packageType,
41
- creditLimit,
42
- source,
43
- type: purchaseType,
44
- platform,
45
- appVersion,
46
- timestamp: Timestamp.fromDate(new Date()),
47
- };
33
+ const newMetadata: PurchaseMetadata = {
34
+ productId,
35
+ packageType,
36
+ creditLimit,
37
+ source,
38
+ type: purchaseType,
39
+ platform,
40
+ appVersion,
41
+ timestamp: Timestamp.fromDate(new Date()),
42
+ };
48
43
 
49
- const purchaseHistory = [...existingData.purchaseHistory, newMetadata].slice(-10);
44
+ const purchaseHistory = [...existingData.purchaseHistory, newMetadata].slice(-10);
50
45
 
51
- return { purchaseType, purchaseHistory };
52
- }
46
+ return { purchaseType, purchaseHistory };
53
47
  }
@@ -1,24 +1,15 @@
1
1
  import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
2
2
  import { isCreditPackage } from "../../../../utils/packageTypeDetector";
3
3
 
4
- /**
5
- * Default strategy for new purchases, renewals, or upgrades.
6
- * Resets credits for subscriptions, but ADDS credits for consumable packages.
7
- */
8
4
  export class StandardPurchaseCreditStrategy implements ICreditStrategy {
9
5
  canHandle(_params: CreditAllocationParams): boolean {
10
- // This is a catch-all strategy
11
6
  return true;
12
7
  }
13
8
 
14
9
  execute(params: CreditAllocationParams): number {
15
- // If it's a credit package (consumable), we add to existing balance
16
- if (params.productId && isCreditPackage(params.productId)) {
17
- const existing = params.existingData?.credits ?? 0;
18
- return existing + params.creditLimit;
19
- }
20
-
21
- // Standard subscription behavior: Reset to the calculated limit (e.g. 100/mo)
22
- return params.creditLimit;
10
+ const isConsumable = params.productId && isCreditPackage(params.productId);
11
+ return isConsumable
12
+ ? (params.existingData?.credits ?? 0) + params.creditLimit
13
+ : params.creditLimit;
23
14
  }
24
15
  }
@@ -5,13 +5,14 @@
5
5
 
6
6
  import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
7
7
  import { serverTimestamp, type DocumentSnapshot } from "firebase/firestore";
8
+ import { SUBSCRIPTION_STATUS, type Platform } from "../../subscription/core/SubscriptionConstants";
8
9
 
9
10
  /**
10
11
  * Get existing credit document or create default
11
12
  */
12
13
  export function getCreditDocumentOrDefault(
13
14
  creditsDoc: DocumentSnapshot,
14
- platform: "ios" | "android"
15
+ platform: Platform
15
16
  ): UserCreditsDocumentRead {
16
17
  if (creditsDoc.exists()) {
17
18
  return creditsDoc.data() as UserCreditsDocumentRead;
@@ -23,7 +24,7 @@ export function getCreditDocumentOrDefault(
23
24
  credits: 0,
24
25
  creditLimit: 0,
25
26
  isPremium: false,
26
- status: "none",
27
+ status: SUBSCRIPTION_STATUS.NONE,
27
28
  processedPurchases: [],
28
29
  purchaseHistory: [],
29
30
  platform,
@@ -42,6 +43,8 @@ export function getCreditDocumentOrDefault(
42
43
  trialEndDate: null,
43
44
  trialCredits: 0,
44
45
  convertedFromTrial: false,
46
+ purchaseSource: null,
47
+ purchaseType: null,
45
48
  } as any;
46
49
  }
47
50
 
@@ -2,25 +2,27 @@ import { Timestamp, serverTimestamp } from "firebase/firestore";
2
2
  import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
3
3
  import { creditAllocationOrchestrator } from "./credit-strategies/CreditAllocationOrchestrator";
4
4
  import { isPast } from "../../../utils/dateUtils";
5
-
5
+ import { isCreditPackage } from "../../../utils/packageTypeDetector";
6
6
  import {
7
7
  CalculateCreditsParams,
8
8
  BuildCreditsDataParams
9
9
  } from "./creditOperationUtils.types";
10
+ import { PURCHASE_ID_PREFIXES } from "../core/CreditsConstants";
11
+
10
12
 
11
13
  export function calculateNewCredits({ metadata, existingData, creditLimit, purchaseId }: CalculateCreditsParams): number {
12
- const isPremium = metadata.isPremium;
13
14
  const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
15
+ const isPremium = metadata.isPremium;
14
16
  const status = resolveSubscriptionStatus({
15
17
  isPremium,
16
18
  willRenew: metadata.willRenew ?? false,
17
19
  isExpired,
18
20
  periodType: metadata.periodType ?? undefined,
19
21
  });
20
- const isStatusSync = purchaseId.startsWith("status_sync_");
22
+
21
23
  return creditAllocationOrchestrator.allocate({
22
24
  status,
23
- isStatusSync,
25
+ isStatusSync: purchaseId.startsWith(PURCHASE_ID_PREFIXES.STATUS_SYNC),
24
26
  existingData,
25
27
  creditLimit,
26
28
  isSubscriptionActive: isPremium && !isExpired,
@@ -31,8 +33,13 @@ export function calculateNewCredits({ metadata, existingData, creditLimit, purch
31
33
  export function buildCreditsData({
32
34
  existingData, newCredits, creditLimit, purchaseId, metadata, purchaseHistory, platform
33
35
  }: BuildCreditsDataParams): Record<string, any> {
34
- const isPremium = metadata.isPremium;
36
+ const isConsumable = isCreditPackage(metadata.productId ?? "");
37
+ const isPremium = isConsumable ? (existingData?.isPremium ?? metadata.isPremium) : metadata.isPremium;
35
38
  const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
39
+ const resolvedCreditLimit = isConsumable
40
+ ? (existingData?.creditLimit || creditLimit)
41
+ : creditLimit;
42
+
36
43
  const status = resolveSubscriptionStatus({
37
44
  isPremium,
38
45
  willRenew: metadata.willRenew ?? false,
@@ -40,24 +47,24 @@ export function buildCreditsData({
40
47
  periodType: metadata.periodType ?? undefined,
41
48
  });
42
49
 
43
- const creditsData: Record<string, any> = {
50
+ const isPurchaseOrRenewal = purchaseId.startsWith(PURCHASE_ID_PREFIXES.PURCHASE) ||
51
+ purchaseId.startsWith(PURCHASE_ID_PREFIXES.RENEWAL);
52
+
53
+ return {
44
54
  isPremium,
45
55
  status,
46
56
  credits: newCredits,
47
- creditLimit,
57
+ creditLimit: resolvedCreditLimit,
48
58
  lastUpdatedAt: serverTimestamp(),
49
59
  processedPurchases: [...(existingData?.processedPurchases ?? []), purchaseId].slice(-50),
50
60
  productId: metadata.productId,
51
61
  platform,
62
+ ...(purchaseHistory.length > 0 && { purchaseHistory }),
63
+ ...(isPurchaseOrRenewal && { lastPurchaseAt: serverTimestamp() }),
64
+ ...(metadata.expirationDate && { expirationDate: Timestamp.fromDate(new Date(metadata.expirationDate)) }),
65
+ ...(metadata.willRenew !== undefined && { willRenew: metadata.willRenew }),
66
+ ...(metadata.originalTransactionId && { originalTransactionId: metadata.originalTransactionId }),
52
67
  };
53
-
54
- if (purchaseHistory.length > 0) creditsData.purchaseHistory = purchaseHistory;
55
- if (purchaseId.startsWith("purchase_") || purchaseId.startsWith("renewal_")) creditsData.lastPurchaseAt = serverTimestamp();
56
- if (metadata.expirationDate) creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
57
- if (metadata.willRenew !== undefined) creditsData.willRenew = metadata.willRenew;
58
- if (metadata.originalTransactionId) creditsData.originalTransactionId = metadata.originalTransactionId;
59
-
60
- return creditsData;
61
68
  }
62
69
 
63
70
  export function shouldSkipStatusSyncWrite(
@@ -65,12 +72,11 @@ export function shouldSkipStatusSyncWrite(
65
72
  existingData: any,
66
73
  newCreditsData: Record<string, any>
67
74
  ): boolean {
68
- if (!purchaseId.startsWith("status_sync_")) return false;
69
- return (
70
- existingData.isPremium === newCreditsData.isPremium &&
75
+ if (!purchaseId.startsWith(PURCHASE_ID_PREFIXES.STATUS_SYNC)) return false;
76
+
77
+ return existingData.isPremium === newCreditsData.isPremium &&
71
78
  existingData.status === newCreditsData.status &&
72
79
  existingData.credits === newCreditsData.credits &&
73
80
  existingData.creditLimit === newCreditsData.creditLimit &&
74
- existingData.productId === newCreditsData.productId
75
- );
81
+ existingData.productId === newCreditsData.productId;
76
82
  }
@@ -1,5 +1,6 @@
1
1
  import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
2
2
  import type { InitializeCreditsMetadata } from "../../subscription/application/SubscriptionInitializerTypes";
3
+ import type { Platform } from "../../subscription/core/SubscriptionConstants";
3
4
 
4
5
  export interface CalculateCreditsParams {
5
6
  metadata: InitializeCreditsMetadata;
@@ -15,5 +16,5 @@ export interface BuildCreditsDataParams {
15
16
  purchaseId: string;
16
17
  metadata: InitializeCreditsMetadata;
17
18
  purchaseHistory: any[];
18
- platform: "ios" | "android";
19
+ platform: Platform;
19
20
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Credit Error Codes
3
+ */
4
+ export const CREDIT_ERROR_CODES = {
5
+ NO_CREDITS: 'NO_CREDITS',
6
+ CREDITS_EXHAUSTED: 'CREDITS_EXHAUSTED',
7
+ DEDUCT_ERR: 'DEDUCT_ERR',
8
+ DB_ERROR: 'ERR',
9
+ } as const;
10
+
11
+ /**
12
+ * Purchase ID Prefixes
13
+ */
14
+ export const PURCHASE_ID_PREFIXES = {
15
+ STATUS_SYNC: 'status_sync_',
16
+ PURCHASE: 'purchase_',
17
+ RENEWAL: 'renewal_',
18
+ } as const;
@@ -2,76 +2,64 @@ import type { UserCredits } from "./Credits";
2
2
  import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
3
3
  import type { PeriodType, SubscriptionStatusType } from "../../subscription/core/SubscriptionConstants";
4
4
  import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
5
-
6
5
  import { toSafeDate } from "../../../utils/dateUtils";
7
6
 
8
- /** Maps Firestore document to domain entity with expiration validation */
9
- export class CreditsMapper {
10
- static toEntity(doc: UserCreditsDocumentRead): UserCredits {
11
- const expirationDate = toSafeDate(doc.expirationDate);
12
- const periodType = doc.periodType;
13
-
14
- // Validate isPremium against expirationDate (real-time check)
15
- const { isPremium, status } = CreditsMapper.validateSubscription(doc, expirationDate, periodType);
16
-
17
- return {
18
- // Core subscription (validated)
19
- isPremium,
20
- status,
21
-
22
- // Dates
23
- purchasedAt: toSafeDate(doc.purchasedAt) ?? new Date(),
24
- expirationDate,
25
- lastUpdatedAt: toSafeDate(doc.lastUpdatedAt) ?? new Date(),
26
- lastPurchaseAt: toSafeDate(doc.lastPurchaseAt),
7
+ /**
8
+ * Validate subscription status against expirationDate and periodType
9
+ */
10
+ function validateSubscription(
11
+ doc: UserCreditsDocumentRead,
12
+ expirationDate: Date | null,
13
+ periodType: PeriodType | null
14
+ ): { isPremium: boolean; status: SubscriptionStatusType } {
15
+ const isPremium = doc.isPremium;
16
+ const willRenew = doc.willRenew ?? false;
17
+ const isExpired = expirationDate ? expirationDate < new Date() : false;
27
18
 
28
- // RevenueCat details
29
- willRenew: doc.willRenew,
30
- productId: doc.productId,
31
- packageType: doc.packageType,
32
- originalTransactionId: doc.originalTransactionId,
19
+ const status = resolveSubscriptionStatus({
20
+ isPremium,
21
+ willRenew,
22
+ isExpired,
23
+ periodType: periodType ?? undefined,
24
+ });
33
25
 
34
- // Trial fields
35
- periodType,
36
- isTrialing: doc.isTrialing,
37
- trialStartDate: toSafeDate(doc.trialStartDate),
38
- trialEndDate: toSafeDate(doc.trialEndDate),
39
- trialCredits: doc.trialCredits,
40
- convertedFromTrial: doc.convertedFromTrial,
41
-
42
- // Credits
43
- credits: doc.credits,
44
- creditLimit: doc.creditLimit,
45
-
46
- // Metadata
47
- purchaseSource: doc.purchaseSource,
48
- purchaseType: doc.purchaseType,
49
- platform: doc.platform,
50
- appVersion: doc.appVersion,
51
- };
52
- }
26
+ return {
27
+ isPremium: isExpired ? false : isPremium,
28
+ status,
29
+ };
30
+ }
53
31
 
54
- /** Validate subscription status against expirationDate and periodType */
55
- private static validateSubscription(
56
- doc: UserCreditsDocumentRead,
57
- expirationDate: Date | null,
58
- periodType: PeriodType | null
59
- ): { isPremium: boolean; status: SubscriptionStatusType } {
60
- const isPremium = doc.isPremium;
61
- const willRenew = doc.willRenew ?? false;
62
- const isExpired = expirationDate ? expirationDate < new Date() : false;
32
+ /**
33
+ * Maps Firestore document to domain entity with expiration validation
34
+ */
35
+ export function mapCreditsDocumentToEntity(doc: UserCreditsDocumentRead): UserCredits {
36
+ const expirationDate = toSafeDate(doc.expirationDate);
37
+ const periodType = doc.periodType;
63
38
 
64
- const status = resolveSubscriptionStatus({
65
- isPremium,
66
- willRenew,
67
- isExpired,
68
- periodType: periodType ?? undefined,
69
- });
39
+ const { isPremium, status } = validateSubscription(doc, expirationDate, periodType);
70
40
 
71
- // Override isPremium if expired
72
- return {
73
- isPremium: isExpired ? false : isPremium,
74
- status,
75
- };
76
- }
41
+ return {
42
+ isPremium,
43
+ status,
44
+ purchasedAt: toSafeDate(doc.purchasedAt) ?? new Date(),
45
+ expirationDate,
46
+ lastUpdatedAt: toSafeDate(doc.lastUpdatedAt) ?? new Date(),
47
+ lastPurchaseAt: toSafeDate(doc.lastPurchaseAt),
48
+ willRenew: doc.willRenew,
49
+ productId: doc.productId,
50
+ packageType: doc.packageType,
51
+ originalTransactionId: doc.originalTransactionId,
52
+ periodType,
53
+ isTrialing: doc.isTrialing,
54
+ trialStartDate: toSafeDate(doc.trialStartDate),
55
+ trialEndDate: toSafeDate(doc.trialEndDate),
56
+ trialCredits: doc.trialCredits,
57
+ convertedFromTrial: doc.convertedFromTrial,
58
+ credits: doc.credits,
59
+ creditLimit: doc.creditLimit,
60
+ purchaseSource: doc.purchaseSource,
61
+ purchaseType: doc.purchaseType,
62
+ platform: doc.platform,
63
+ appVersion: doc.appVersion,
64
+ };
77
65
  }
@@ -1,36 +1,33 @@
1
- /**
2
- * Credits Repository
3
- * Optimized to use Design Patterns: Command, Observer, and Strategy.
4
- */
5
-
6
- import { getDoc, setDoc, type Firestore } from "firebase/firestore";
1
+ import { getDoc, setDoc, type Firestore, type DocumentReference } from "firebase/firestore";
7
2
  import { BaseRepository } from "@umituz/react-native-firebase";
8
3
  import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
9
4
  import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
10
5
  import { initializeCreditsTransaction } from "../application/CreditsInitializer";
11
- import { CreditsMapper } from "../core/CreditsMapper";
6
+ import { mapCreditsDocumentToEntity } from "../core/CreditsMapper";
12
7
  import type { RevenueCatData } from "../../subscription/core/RevenueCatData";
13
- import { DeductCreditsCommand } from "../application/DeductCreditsCommand";
14
- import { CreditLimitCalculator } from "../application/CreditLimitCalculator";
8
+ import { deductCreditsOperation } from "../application/DeductCreditsCommand";
9
+ import { calculateCreditLimit } from "../application/CreditLimitCalculator";
15
10
  import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
16
11
  import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
12
+ import { SUBSCRIPTION_STATUS } from "../../subscription/core/SubscriptionConstants";
17
13
 
14
+ /**
15
+ * Credits Repository
16
+ * Provides domain-specific database operations for credits system.
17
+ */
18
18
  export class CreditsRepository extends BaseRepository {
19
- private deductCommand: DeductCreditsCommand;
20
-
21
19
  constructor(private config: CreditsConfig) {
22
20
  super();
23
- this.deductCommand = new DeductCreditsCommand((db, uid) => this.getRef(db, uid));
24
21
  }
25
22
 
26
23
  private getCollectionConfig(): CollectionConfig {
27
24
  return {
28
- collectionName: "credits",
25
+ collectionName: this.config.collectionName,
29
26
  useUserSubcollection: this.config.useUserSubcollection,
30
27
  };
31
28
  }
32
29
 
33
- private getRef(db: Firestore, userId: string) {
30
+ private getRef(db: Firestore, userId: string): DocumentReference {
34
31
  const config = this.getCollectionConfig();
35
32
  return buildDocRef(db, userId, "balance", config);
36
33
  }
@@ -43,7 +40,7 @@ export class CreditsRepository extends BaseRepository {
43
40
  return { success: true, data: null, error: null };
44
41
  }
45
42
 
46
- const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
43
+ const entity = mapCreditsDocumentToEntity(snap.data() as UserCreditsDocumentRead);
47
44
  return { success: true, data: entity, error: null };
48
45
  }
49
46
 
@@ -56,7 +53,7 @@ export class CreditsRepository extends BaseRepository {
56
53
  type: PurchaseType = PURCHASE_TYPE.INITIAL
57
54
  ): Promise<CreditsResult> {
58
55
  const db = requireFirestore();
59
- const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
56
+ const creditLimit = calculateCreditLimit(productId, this.config);
60
57
  const cfg = { ...this.config, creditLimit };
61
58
 
62
59
  const result = await initializeCreditsTransaction(
@@ -78,16 +75,17 @@ export class CreditsRepository extends BaseRepository {
78
75
 
79
76
  return {
80
77
  success: true,
81
- data: result.finalData ? CreditsMapper.toEntity(result.finalData) : null,
78
+ data: result.finalData ? mapCreditsDocumentToEntity(result.finalData) : null,
82
79
  error: null,
83
80
  };
84
81
  }
85
82
 
86
83
  /**
87
- * Delegates to DeductCreditsCommand (Command Pattern)
84
+ * Deducts credits using atomic transaction logic.
88
85
  */
89
86
  async deductCredit(userId: string, cost: number): Promise<DeductCreditsResult> {
90
- return this.deductCommand.execute(userId, cost);
87
+ const db = requireFirestore();
88
+ return deductCreditsOperation(db, this.getRef(db, userId), cost, userId);
91
89
  }
92
90
 
93
91
  async hasCredits(userId: string, cost: number): Promise<boolean> {
@@ -101,7 +99,7 @@ export class CreditsRepository extends BaseRepository {
101
99
  const ref = this.getRef(db, userId);
102
100
  await setDoc(ref, {
103
101
  isPremium: false,
104
- status: "expired",
102
+ status: SUBSCRIPTION_STATUS.EXPIRED,
105
103
  willRenew: false,
106
104
  expirationDate: new Date().toISOString()
107
105
  }, { merge: true });
@@ -8,7 +8,7 @@ import {
8
8
  getCreditsConfig,
9
9
  isCreditsRepositoryConfigured,
10
10
  } from "../infrastructure/CreditsRepositoryManager";
11
- import { calculateCreditPercentage, canAffordCost } from "../utils/creditCalculations";
11
+ import { calculateCreditPercentage, canAfford as canAffordCheck } from "../../../shared/utils/numberUtils";
12
12
 
13
13
  export const creditsQueryKeys = {
14
14
  all: ["credits"] as const,
@@ -90,7 +90,7 @@ export const useCredits = (): UseCreditsResult => {
90
90
  }, [credits, config?.creditLimit]);
91
91
 
92
92
  const canAfford = useCallback(
93
- (cost: number): boolean => canAffordCost(credits?.credits, cost),
93
+ (cost: number): boolean => canAffordCheck(credits?.credits, cost),
94
94
  [credits]
95
95
  );
96
96
 
@@ -8,7 +8,7 @@ import { useMutation, useQueryClient } from "@umituz/react-native-design-system"
8
8
  import type { UserCredits } from "../core/Credits";
9
9
  import { getCreditsRepository } from "../infrastructure/CreditsRepositoryManager";
10
10
  import { creditsQueryKeys } from "./useCredits";
11
- import { calculateRemainingCredits } from "../utils/creditCalculations";
11
+ import { calculateRemaining } from "../../../shared/utils/numberUtils";
12
12
 
13
13
  import { timezoneService } from "@umituz/react-native-design-system";
14
14
 
@@ -48,7 +48,7 @@ export const useDeductCredit = ({
48
48
  }
49
49
 
50
50
  // Calculate new credits using utility
51
- const newCredits = calculateRemainingCredits(previousCredits.credits, cost);
51
+ const newCredits = calculateRemaining(previousCredits.credits, cost);
52
52
 
53
53
  queryClient.setQueryData<UserCredits | null>(creditsQueryKeys.user(userId), (old) => {
54
54
  if (!old) return old;
@@ -60,8 +60,8 @@ export const PURCHASE_TYPE = {
60
60
  INITIAL: 'initial',
61
61
  RENEWAL: 'renewal',
62
62
  UPGRADE: 'upgrade',
63
- DOWNGRADE: 'downgrade',
64
63
  } as const;
65
64
 
66
65
  export type PurchaseType = (typeof PURCHASE_TYPE)[keyof typeof PURCHASE_TYPE];
67
66
 
67
+
@@ -1,28 +0,0 @@
1
- /**
2
- * Credit Calculation Utilities
3
- * Centralized logic for credit mathematical operations
4
- * Uses shared number utilities for consistency
5
- */
6
-
7
- import { calculateCreditPercentage as calcPct, canAfford as canAffordCheck, calculateRemaining } from "../../../shared/utils/numberUtils";
8
-
9
- export const calculateCreditPercentage = (
10
- currentCredits: number | null | undefined,
11
- creditLimit: number
12
- ): number => {
13
- return calcPct(currentCredits, creditLimit);
14
- };
15
-
16
- export const canAffordCost = (
17
- currentCredits: number | null | undefined,
18
- cost: number
19
- ): boolean => {
20
- return canAffordCheck(currentCredits, cost);
21
- };
22
-
23
- export const calculateRemainingCredits = (
24
- currentCredits: number,
25
- cost: number
26
- ): number => {
27
- return calculateRemaining(currentCredits, cost);
28
- };