@umituz/react-native-subscription 2.27.64 → 2.27.66

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.64",
3
+ "version": "2.27.66",
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",
@@ -55,7 +55,9 @@
55
55
  "@tanstack/query-async-storage-persister": "^5.66.7",
56
56
  "@tanstack/react-query": "^5.0.0",
57
57
  "@tanstack/react-query-persist-client": "^5.66.7",
58
+ "@types/jest": "^30.0.0",
58
59
  "@types/react": "~19.1.10",
60
+ "@types/react-native": "^0.72.8",
59
61
  "@typescript-eslint/eslint-plugin": "^8.50.1",
60
62
  "@typescript-eslint/parser": "^8.50.1",
61
63
  "@umituz/react-native-auth": "^3.6.14",
@@ -6,21 +6,17 @@
6
6
  */
7
7
 
8
8
  import type { SubscriptionPackageType } from "../../utils/packageTypeDetector";
9
- import type { SubscriptionStatusType, PeriodType } from "./SubscriptionStatus";
9
+ import type {
10
+ SubscriptionStatusType,
11
+ PeriodType,
12
+ PackageType,
13
+ Platform,
14
+ PurchaseSource,
15
+ PurchaseType
16
+ } from "./SubscriptionConstants";
10
17
 
11
18
  export type CreditType = "text" | "image";
12
19
 
13
- export type PurchaseSource =
14
- | "onboarding"
15
- | "settings"
16
- | "upgrade_prompt"
17
- | "home_screen"
18
- | "feature_gate"
19
- | "credits_exhausted"
20
- | "renewal";
21
-
22
- export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
23
-
24
20
  /** Single Source of Truth for user subscription + credits data */
25
21
  export interface UserCredits {
26
22
  // Core subscription
@@ -35,7 +31,7 @@ export interface UserCredits {
35
31
  // RevenueCat subscription details
36
32
  willRenew: boolean;
37
33
  productId?: string;
38
- packageType?: "weekly" | "monthly" | "yearly" | "lifetime";
34
+ packageType?: PackageType;
39
35
  originalTransactionId?: string;
40
36
 
41
37
  // Trial fields
@@ -53,7 +49,7 @@ export interface UserCredits {
53
49
  // Metadata
54
50
  purchaseSource?: PurchaseSource;
55
51
  purchaseType?: PurchaseType;
56
- platform?: "ios" | "android";
52
+ platform?: Platform;
57
53
  appVersion?: string;
58
54
  }
59
55
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Subscription Constants and Types
3
+ * Centralized source of truth for subscription-related enums and types.
4
+ */
5
+
6
+ /** Subscription status constants */
7
+ export const SUBSCRIPTION_STATUS = {
8
+ ACTIVE: 'active',
9
+ TRIAL: 'trial',
10
+ TRIAL_CANCELED: 'trial_canceled',
11
+ EXPIRED: 'expired',
12
+ CANCELED: 'canceled',
13
+ NONE: 'none',
14
+ } as const;
15
+
16
+ export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
17
+
18
+ /** RevenueCat period type constants */
19
+ export const PERIOD_TYPE = {
20
+ NORMAL: 'NORMAL',
21
+ INTRO: 'INTRO',
22
+ TRIAL: 'TRIAL',
23
+ } as const;
24
+
25
+ export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE];
26
+
27
+ /** Subscription package type constants */
28
+ export const PACKAGE_TYPE = {
29
+ WEEKLY: 'weekly',
30
+ MONTHLY: 'monthly',
31
+ YEARLY: 'yearly',
32
+ LIFETIME: 'lifetime',
33
+ UNKNOWN: 'unknown',
34
+ } as const;
35
+
36
+ export type PackageType = (typeof PACKAGE_TYPE)[keyof typeof PACKAGE_TYPE];
37
+
38
+ /** Platform constants */
39
+ export const PLATFORM = {
40
+ IOS: 'ios',
41
+ ANDROID: 'android',
42
+ } as const;
43
+
44
+ export type Platform = (typeof PLATFORM)[keyof typeof PLATFORM];
45
+
46
+ /** Purchase source constants */
47
+ export const PURCHASE_SOURCE = {
48
+ ONBOARDING: 'onboarding',
49
+ SETTINGS: 'settings',
50
+ UPGRADE_PROMPT: 'upgrade_prompt',
51
+ HOME_SCREEN: 'home_screen',
52
+ FEATURE_GATE: 'feature_gate',
53
+ CREDITS_EXHAUSTED: 'credits_exhausted',
54
+ RENEWAL: 'renewal',
55
+ } as const;
56
+
57
+ export type PurchaseSource = (typeof PURCHASE_SOURCE)[keyof typeof PURCHASE_SOURCE];
58
+
59
+ /** Purchase type constants */
60
+ export const PURCHASE_TYPE = {
61
+ INITIAL: 'initial',
62
+ RENEWAL: 'renewal',
63
+ UPGRADE: 'upgrade',
64
+ DOWNGRADE: 'downgrade',
65
+ } as const;
66
+
67
+ export type PurchaseType = (typeof PURCHASE_TYPE)[keyof typeof PURCHASE_TYPE];
@@ -1,7 +1,11 @@
1
+ import {
2
+ SUBSCRIPTION_STATUS,
3
+ } from './SubscriptionConstants';
1
4
  import {
2
5
  createDefaultSubscriptionStatus,
3
6
  isSubscriptionValid,
4
7
  calculateDaysRemaining,
8
+ resolveSubscriptionStatus,
5
9
  } from './SubscriptionStatus';
6
10
 
7
11
  describe('SubscriptionStatus', () => {
@@ -16,7 +20,7 @@ describe('SubscriptionStatus', () => {
16
20
  purchasedAt: null,
17
21
  customerId: null,
18
22
  syncedAt: null,
19
- status: 'none',
23
+ status: SUBSCRIPTION_STATUS.NONE,
20
24
  });
21
25
  });
22
26
  });
@@ -102,4 +106,44 @@ describe('SubscriptionStatus', () => {
102
106
  expect(calculateDaysRemaining(pastDate.toISOString())).toBe(0);
103
107
  });
104
108
  });
109
+
110
+ describe('resolveSubscriptionStatus', () => {
111
+ it('should return NONE when not premium', () => {
112
+ expect(resolveSubscriptionStatus({ isPremium: false })).toBe(SUBSCRIPTION_STATUS.NONE);
113
+ });
114
+
115
+ it('should return EXPIRED when expired', () => {
116
+ expect(resolveSubscriptionStatus({ isPremium: true, isExpired: true })).toBe(SUBSCRIPTION_STATUS.EXPIRED);
117
+ });
118
+
119
+ it('should return TRIAL when trialing and set to renew', () => {
120
+ expect(resolveSubscriptionStatus({
121
+ isPremium: true,
122
+ periodType: 'TRIAL',
123
+ willRenew: true
124
+ })).toBe(SUBSCRIPTION_STATUS.TRIAL);
125
+ });
126
+
127
+ it('should return TRIAL_CANCELED when trialing and will not renew', () => {
128
+ expect(resolveSubscriptionStatus({
129
+ isPremium: true,
130
+ periodType: 'TRIAL',
131
+ willRenew: false
132
+ })).toBe(SUBSCRIPTION_STATUS.TRIAL_CANCELED);
133
+ });
134
+
135
+ it('should return ACTIVE when premium, not expired, not trial, and set to renew', () => {
136
+ expect(resolveSubscriptionStatus({
137
+ isPremium: true,
138
+ willRenew: true
139
+ })).toBe(SUBSCRIPTION_STATUS.ACTIVE);
140
+ });
141
+
142
+ it('should return CANCELED when premium, not expired, not trial, and will not renew', () => {
143
+ expect(resolveSubscriptionStatus({
144
+ isPremium: true,
145
+ willRenew: false
146
+ })).toBe(SUBSCRIPTION_STATUS.CANCELED);
147
+ });
148
+ });
105
149
  });
@@ -1,24 +1,17 @@
1
1
  import { timezoneService } from "@umituz/react-native-design-system";
2
+ import {
3
+ SUBSCRIPTION_STATUS,
4
+ PERIOD_TYPE,
5
+ type PeriodType,
6
+ type SubscriptionStatusType
7
+ } from "./SubscriptionConstants";
2
8
 
3
- export const SUBSCRIPTION_STATUS = {
4
- ACTIVE: 'active',
5
- TRIAL: 'trial',
6
- TRIAL_CANCELED: 'trial_canceled',
7
- EXPIRED: 'expired',
8
- CANCELED: 'canceled',
9
- NONE: 'none',
10
- } as const;
11
-
12
- /** RevenueCat period type constants */
13
- export const PERIOD_TYPE = {
14
- NORMAL: 'NORMAL',
15
- INTRO: 'INTRO',
16
- TRIAL: 'TRIAL',
17
- } as const;
18
-
19
- export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE];
20
-
21
- export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
9
+ export {
10
+ SUBSCRIPTION_STATUS,
11
+ PERIOD_TYPE,
12
+ type PeriodType,
13
+ type SubscriptionStatusType
14
+ };
22
15
 
23
16
  export interface SubscriptionStatus {
24
17
  isPremium: boolean;
package/src/index.ts CHANGED
@@ -6,15 +6,14 @@ export * from "./domains/wallet";
6
6
  export * from "./domains/paywall";
7
7
  export * from "./domains/config";
8
8
 
9
- // Domain Layer
9
+ // Domain Layer - Constants & Types
10
+ export * from "./domain/entities/SubscriptionConstants";
10
11
  export {
11
- SUBSCRIPTION_STATUS,
12
- PERIOD_TYPE,
13
12
  createDefaultSubscriptionStatus,
14
13
  isSubscriptionValid,
15
14
  resolveSubscriptionStatus,
16
15
  } from "./domain/entities/SubscriptionStatus";
17
- export type { SubscriptionStatus, SubscriptionStatusType, PeriodType, StatusResolverInput } from "./domain/entities/SubscriptionStatus";
16
+ export type { SubscriptionStatus, StatusResolverInput } from "./domain/entities/SubscriptionStatus";
18
17
  export type { SubscriptionConfig } from "./domain/value-objects/SubscriptionConfig";
19
18
  export type { ISubscriptionRepository } from "./application/ports/ISubscriptionRepository";
20
19
 
@@ -78,15 +77,12 @@ export * from "./presentation/types/SubscriptionDetailTypes";
78
77
  // Presentation Layer - Stores
79
78
  export * from "./presentation/stores";
80
79
 
81
- // Credits Domain
82
80
  export type {
83
81
  CreditType,
84
82
  UserCredits,
85
83
  CreditsConfig,
86
84
  CreditsResult,
87
85
  DeductCreditsResult,
88
- PurchaseSource,
89
- PurchaseType,
90
86
  CreditAllocation,
91
87
  PackageAllocationMap,
92
88
  } from "./domain/entities/Credits";
@@ -1,5 +1,6 @@
1
1
  import type { UserCredits } from "../../domain/entities/Credits";
2
- import { resolveSubscriptionStatus, type PeriodType, type SubscriptionStatusType } from "../../domain/entities/SubscriptionStatus";
2
+ import { resolveSubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
3
+ import type { PeriodType, SubscriptionStatusType } from "../../domain/entities/SubscriptionConstants";
3
4
  import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
4
5
 
5
6
  /** Maps Firestore document to domain entity with expiration validation */
@@ -1,8 +1,18 @@
1
- import type { PurchaseSource, PurchaseType } from "../../domain/entities/Credits";
2
- import type { SubscriptionStatusType, PeriodType } from "../../domain/entities/SubscriptionStatus";
1
+ import type {
2
+ PurchaseSource,
3
+ PurchaseType,
4
+ SubscriptionStatusType,
5
+ PeriodType,
6
+ PackageType,
7
+ Platform
8
+ } from "../../domain/entities/SubscriptionConstants";
3
9
 
4
- export type { PurchaseSource, PurchaseType } from "../../domain/entities/Credits";
5
- export type { SubscriptionStatusType, PeriodType } from "../../domain/entities/SubscriptionStatus";
10
+ export type {
11
+ PurchaseSource,
12
+ PurchaseType,
13
+ SubscriptionStatusType,
14
+ PeriodType
15
+ };
6
16
 
7
17
  export interface FirestoreTimestamp {
8
18
  toDate: () => Date;
@@ -10,11 +20,11 @@ export interface FirestoreTimestamp {
10
20
 
11
21
  export interface PurchaseMetadata {
12
22
  productId: string;
13
- packageType: "weekly" | "monthly" | "yearly" | "lifetime";
23
+ packageType: PackageType;
14
24
  creditLimit: number;
15
25
  source: PurchaseSource;
16
26
  type: PurchaseType;
17
- platform: "ios" | "android";
27
+ platform: Platform;
18
28
  appVersion?: string;
19
29
  timestamp: FirestoreTimestamp;
20
30
  }
@@ -34,7 +44,7 @@ export interface UserCreditsDocumentRead {
34
44
  // RevenueCat subscription details
35
45
  willRenew?: boolean;
36
46
  productId?: string;
37
- packageType?: "weekly" | "monthly" | "yearly" | "lifetime";
47
+ packageType?: PackageType;
38
48
  originalTransactionId?: string;
39
49
 
40
50
  // Trial fields
@@ -52,7 +62,7 @@ export interface UserCreditsDocumentRead {
52
62
  // Metadata
53
63
  purchaseSource?: PurchaseSource;
54
64
  purchaseType?: PurchaseType;
55
- platform?: "ios" | "android";
65
+ platform?: Platform;
56
66
  appVersion?: string;
57
67
  processedPurchases?: string[];
58
68
  purchaseHistory?: PurchaseMetadata[];
@@ -13,8 +13,8 @@ import { configureCreditsRepository, getCreditsRepository } from "../repositorie
13
13
  import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
14
14
  import { configureAuthProvider } from "../../presentation/hooks/useAuthAwarePurchase";
15
15
  import type { RevenueCatData } from "../../domain/types/RevenueCatData";
16
+ import { type PeriodType, type PurchaseSource } from "../../domain/entities/SubscriptionConstants";
16
17
  import type { SubscriptionInitConfig, FirebaseAuthLike } from "./SubscriptionInitializerTypes";
17
- import type { PurchaseSource } from "../../domain/entities/Credits";
18
18
 
19
19
  export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
20
20
 
@@ -53,8 +53,7 @@ const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string
53
53
  expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
54
54
  willRenew: entitlement?.willRenew ?? false,
55
55
  originalTransactionId: entitlement?.originalPurchaseDate ?? undefined,
56
- isPremium: Object.keys(customerInfo.entitlements.active).length > 0,
57
- periodType: entitlement?.periodType as "NORMAL" | "INTRO" | "TRIAL" | undefined,
56
+ periodType: entitlement?.periodType as PeriodType | undefined,
58
57
  };
59
58
  };
60
59
 
@@ -95,7 +94,7 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
95
94
 
96
95
  const onPremiumStatusChanged = async (
97
96
  userId: string, isPremium: boolean, productId?: string,
98
- expiresAt?: string, willRenew?: boolean, periodType?: "NORMAL" | "INTRO" | "TRIAL"
97
+ expiresAt?: string, willRenew?: boolean, periodType?: PeriodType
99
98
  ) => {
100
99
  if (__DEV__) console.log('[SubscriptionInitializer] onPremiumStatusChanged:', { userId, isPremium, productId, willRenew, periodType });
101
100
  try {
@@ -6,7 +6,10 @@
6
6
  import React, { useMemo } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
8
  import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
- import type { SubscriptionStatusType } from "../../../domain/entities/SubscriptionStatus";
9
+ import {
10
+ SUBSCRIPTION_STATUS,
11
+ type SubscriptionStatusType
12
+ } from "../../../domain/entities/SubscriptionConstants";
10
13
 
11
14
  export type { SubscriptionStatusType };
12
15
 
@@ -30,23 +33,23 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
30
33
  }) => {
31
34
  const tokens = useAppDesignTokens();
32
35
 
33
- const labels: Record<SubscriptionStatusType, string> = {
34
- active: activeLabel,
35
- trial: activeLabel,
36
- trial_canceled: trialCanceledLabel ?? canceledLabel,
37
- expired: expiredLabel,
38
- none: noneLabel,
39
- canceled: canceledLabel,
40
- };
36
+ const labels: Record<SubscriptionStatusType, string> = useMemo(() => ({
37
+ [SUBSCRIPTION_STATUS.ACTIVE]: activeLabel,
38
+ [SUBSCRIPTION_STATUS.TRIAL]: activeLabel,
39
+ [SUBSCRIPTION_STATUS.TRIAL_CANCELED]: trialCanceledLabel ?? canceledLabel,
40
+ [SUBSCRIPTION_STATUS.EXPIRED]: expiredLabel,
41
+ [SUBSCRIPTION_STATUS.NONE]: noneLabel,
42
+ [SUBSCRIPTION_STATUS.CANCELED]: canceledLabel,
43
+ }), [activeLabel, trialCanceledLabel, canceledLabel, expiredLabel, noneLabel]);
41
44
 
42
45
  const backgroundColor = useMemo(() => {
43
46
  const colors: Record<SubscriptionStatusType, string> = {
44
- active: tokens.colors.success,
45
- trial: tokens.colors.primary, // Blue/purple for trial
46
- trial_canceled: tokens.colors.warning, // Orange for trial canceled
47
- expired: tokens.colors.error,
48
- none: tokens.colors.textTertiary,
49
- canceled: tokens.colors.warning,
47
+ [SUBSCRIPTION_STATUS.ACTIVE]: tokens.colors.success,
48
+ [SUBSCRIPTION_STATUS.TRIAL]: tokens.colors.primary, // Blue/purple for trial
49
+ [SUBSCRIPTION_STATUS.TRIAL_CANCELED]: tokens.colors.warning, // Orange for trial canceled
50
+ [SUBSCRIPTION_STATUS.EXPIRED]: tokens.colors.error,
51
+ [SUBSCRIPTION_STATUS.NONE]: tokens.colors.textTertiary,
52
+ [SUBSCRIPTION_STATUS.CANCELED]: tokens.colors.warning,
50
53
  };
51
54
  return colors[status];
52
55
  }, [status, tokens.colors]);
@@ -3,36 +3,37 @@
3
3
  * Detects subscription package type from RevenueCat package identifier
4
4
  */
5
5
 
6
- export type SubscriptionPackageType = "weekly" | "monthly" | "yearly" | "unknown";
6
+ import { PACKAGE_TYPE, type PackageType } from "../domain/entities/SubscriptionConstants";
7
7
 
8
+ export type SubscriptionPackageType = PackageType;
9
+
10
+ /**
11
+ * Check if identifier is a credit package (consumable purchase)
12
+ * Credit packages use a different system and don't need type detection
13
+ */
8
14
  /**
9
15
  * Check if identifier is a credit package (consumable purchase)
10
16
  * Credit packages use a different system and don't need type detection
11
17
  */
12
18
  function isCreditPackage(identifier: string): boolean {
13
- return identifier.includes("credit");
19
+ // Matches "credit" as a word or part of a common naming pattern
20
+ return /\bcredit\b|_credit_|-credit-/i.test(identifier) || identifier.toLowerCase().includes("credit");
14
21
  }
15
22
 
16
23
  /**
17
24
  * Detect package type from product identifier
18
- * Supports common RevenueCat naming patterns:
19
- * - premium_weekly, weekly_premium, premium-weekly
20
- * - premium_monthly, monthly_premium, premium-monthly
21
- * - premium_yearly, yearly_premium, premium-yearly, premium_annual, annual_premium
22
- * - preview-product-id (Preview API mode in Expo Go)
23
- *
24
- * Note: Credit packages (consumable purchases) are skipped silently
25
+ * Supports common RevenueCat naming patterns with regex for better accuracy
25
26
  */
26
27
  export function detectPackageType(productIdentifier: string): SubscriptionPackageType {
27
28
  if (!productIdentifier) {
28
- return "unknown";
29
+ return PACKAGE_TYPE.UNKNOWN;
29
30
  }
30
31
 
31
32
  const normalized = productIdentifier.toLowerCase();
32
33
 
33
34
  // Skip credit packages silently - they use creditPackageConfig instead
34
35
  if (isCreditPackage(normalized)) {
35
- return "unknown";
36
+ return PACKAGE_TYPE.UNKNOWN;
36
37
  }
37
38
 
38
39
  // Preview API mode (Expo Go testing)
@@ -40,40 +41,36 @@ export function detectPackageType(productIdentifier: string): SubscriptionPackag
40
41
  if (__DEV__) {
41
42
  console.log("[PackageTypeDetector] Detected: PREVIEW (monthly)");
42
43
  }
43
- return "monthly";
44
+ return PACKAGE_TYPE.MONTHLY;
44
45
  }
45
46
 
46
- // Weekly detection
47
- if (normalized.includes("weekly") || normalized.includes("week")) {
47
+ // Weekly detection: matches "weekly" or "week" as distinct parts of the ID
48
+ if (/\bweekly?\b|_week_|-week-|\.week\./i.test(normalized)) {
48
49
  if (__DEV__) {
49
50
  console.log("[PackageTypeDetector] Detected: WEEKLY");
50
51
  }
51
- return "weekly";
52
+ return PACKAGE_TYPE.WEEKLY;
52
53
  }
53
54
 
54
- // Monthly detection
55
- if (normalized.includes("monthly") || normalized.includes("month")) {
55
+ // Monthly detection: matches "monthly" or "month"
56
+ if (/\bmonthly?\b|_month_|-month-|\.month\./i.test(normalized)) {
56
57
  if (__DEV__) {
57
58
  console.log("[PackageTypeDetector] Detected: MONTHLY");
58
59
  }
59
- return "monthly";
60
+ return PACKAGE_TYPE.MONTHLY;
60
61
  }
61
62
 
62
- // Yearly detection (includes annual)
63
- if (
64
- normalized.includes("yearly") ||
65
- normalized.includes("year") ||
66
- normalized.includes("annual")
67
- ) {
63
+ // Yearly detection: matches "yearly", "year", or "annual"
64
+ if (/\byearly?\b|_year_|-year-|\.year\.|annual/i.test(normalized)) {
68
65
  if (__DEV__) {
69
66
  console.log("[PackageTypeDetector] Detected: YEARLY");
70
67
  }
71
- return "yearly";
68
+ return PACKAGE_TYPE.YEARLY;
72
69
  }
73
70
 
74
71
  if (__DEV__) {
75
72
  console.warn("[PackageTypeDetector] Unknown package type for:", productIdentifier);
76
73
  }
77
74
 
78
- return "unknown";
75
+ return PACKAGE_TYPE.UNKNOWN;
79
76
  }
@@ -1,24 +0,0 @@
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
- ### Package Periods
14
-
15
- ### Error Codes
16
-
17
- ### Credit Limits
18
-
19
- ### Time Periods
20
-
21
- ## Related
22
-
23
- - [Domain README](../README.md)
24
- - [Entities](../entities/README.md)