@umituz/web-payment 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -2
  3. package/dist/billing-adapter.interface-CDkeeowZ.d.mts +94 -0
  4. package/dist/billing-adapter.interface-m7yE50I0.d.ts +94 -0
  5. package/dist/billing-cycle.enum--hJRYlgP.d.mts +10 -0
  6. package/dist/billing-cycle.enum--hJRYlgP.d.ts +10 -0
  7. package/dist/billing-env-D4YjvA3M.d.mts +8 -0
  8. package/dist/billing-env-D4YjvA3M.d.ts +8 -0
  9. package/dist/checkout-metadata-D7Wp_DAD.d.mts +27 -0
  10. package/dist/checkout-metadata-D7Wp_DAD.d.ts +27 -0
  11. package/dist/chunk-2KV7JFYF.mjs +61 -0
  12. package/dist/chunk-2NH5DOLT.mjs +101 -0
  13. package/dist/chunk-3AXPZOCV.mjs +106 -0
  14. package/dist/chunk-4N3BASZ6.mjs +51 -0
  15. package/dist/chunk-NHKK3ENU.mjs +1128 -0
  16. package/dist/chunk-PIWNUAKW.mjs +30 -0
  17. package/dist/chunk-QFB7AOSQ.mjs +33 -0
  18. package/dist/chunk-RQTMBR74.mjs +30 -0
  19. package/dist/chunk-U45FS4FF.mjs +110 -0
  20. package/dist/chunk-X6R76FZT.mjs +0 -0
  21. package/dist/chunk-XQ2HJTTA.mjs +252 -0
  22. package/dist/core.d.mts +17 -0
  23. package/dist/core.d.ts +17 -0
  24. package/dist/core.js +425 -0
  25. package/dist/core.mjs +66 -0
  26. package/dist/http.d.mts +12 -0
  27. package/dist/http.d.ts +12 -0
  28. package/dist/http.js +115 -0
  29. package/dist/http.mjs +88 -0
  30. package/dist/index.d.mts +17 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.js +1929 -0
  33. package/dist/index.mjs +153 -0
  34. package/dist/memory.d.mts +14 -0
  35. package/dist/memory.d.ts +14 -0
  36. package/dist/memory.js +130 -0
  37. package/dist/memory.mjs +6 -0
  38. package/dist/portal.d.mts +445 -0
  39. package/dist/portal.d.ts +445 -0
  40. package/dist/portal.js +2927 -0
  41. package/dist/portal.mjs +2737 -0
  42. package/dist/product.mapper-CnY1ISOp.d.ts +97 -0
  43. package/dist/product.mapper-noVl-sel.d.mts +97 -0
  44. package/dist/products.d.mts +64 -0
  45. package/dist/products.d.ts +64 -0
  46. package/dist/products.js +370 -0
  47. package/dist/products.mjs +21 -0
  48. package/dist/react.d.mts +182 -0
  49. package/dist/react.d.ts +182 -0
  50. package/dist/react.js +1874 -0
  51. package/dist/react.mjs +105 -0
  52. package/dist/server.d.mts +177 -0
  53. package/dist/server.d.ts +177 -0
  54. package/dist/server.js +622 -0
  55. package/dist/server.mjs +417 -0
  56. package/dist/visual-tokens-BCLVR85G.d.mts +94 -0
  57. package/dist/visual-tokens-BCLVR85G.d.ts +94 -0
  58. package/package.json +122 -4
  59. package/src/core.ts +81 -0
  60. package/src/domain/entities/cancellation.entity.ts +28 -0
  61. package/src/domain/entities/checkout.entity.ts +21 -0
  62. package/src/domain/entities/index.ts +10 -0
  63. package/src/domain/entities/order.entity.ts +15 -0
  64. package/src/domain/entities/product.entity.ts +16 -0
  65. package/src/domain/entities/subscription.entity.ts +24 -0
  66. package/src/domain/entities/sync.entity.ts +4 -0
  67. package/src/domain/enums/amount-type.enum.ts +10 -0
  68. package/src/domain/enums/billing-cycle.enum.ts +30 -0
  69. package/src/domain/enums/index.ts +5 -0
  70. package/src/domain/enums/payment-provider.enum.ts +14 -0
  71. package/src/domain/enums/subscription-status.enum.ts +32 -0
  72. package/src/domain/index.ts +5 -0
  73. package/src/domain/interfaces/billing-adapter.interface.ts +18 -0
  74. package/src/domain/interfaces/index.ts +1 -0
  75. package/src/domain/mappers/product.mapper.ts +176 -0
  76. package/src/domain/utils/minor-units.ts +20 -0
  77. package/src/domain/utils/percent-discount.ts +17 -0
  78. package/src/http.ts +3 -0
  79. package/src/index.ts +12 -0
  80. package/src/infrastructure/browser/hosted-checkout.ts +26 -0
  81. package/src/infrastructure/config/billing-env.ts +31 -0
  82. package/src/infrastructure/constants/billing.constants.ts +10 -0
  83. package/src/infrastructure/http/http-client.ts +83 -0
  84. package/src/infrastructure/pagination/page-iterator.ts +43 -0
  85. package/src/infrastructure/query/query-builder.ts +26 -0
  86. package/src/infrastructure/services/billing.client.ts +300 -0
  87. package/src/infrastructure/services/create-billing-client.ts +23 -0
  88. package/src/infrastructure/services/http/create-http-adapter.ts +123 -0
  89. package/src/infrastructure/services/memory/create-memory-adapter.ts +126 -0
  90. package/src/infrastructure/services/product.repository.ts +198 -0
  91. package/src/infrastructure/utils/checkout-metadata.ts +55 -0
  92. package/src/infrastructure/utils/date-renderer.ts +53 -0
  93. package/src/infrastructure/utils/money-formatter.ts +39 -0
  94. package/src/infrastructure/utils/normalization.util.ts +22 -0
  95. package/src/infrastructure/utils/pagination-clamp.ts +11 -0
  96. package/src/infrastructure/utils/receipt.util.ts +6 -0
  97. package/src/infrastructure/utils/security.util.ts +23 -0
  98. package/src/infrastructure/utils/type-guards.util.ts +23 -0
  99. package/src/infrastructure/utils/validation.util.ts +10 -0
  100. package/src/infrastructure/webhook/parse-event.ts +135 -0
  101. package/src/infrastructure/webhook/verify-webhook.ts +124 -0
  102. package/src/memory.ts +3 -0
  103. package/src/portal.ts +2 -0
  104. package/src/presentation/components/BillingHistoryList.tsx +172 -0
  105. package/src/presentation/components/BillingProvider.tsx +179 -0
  106. package/src/presentation/components/CancelSubscriptionButton.tsx +155 -0
  107. package/src/presentation/components/CheckoutButton.tsx +76 -0
  108. package/src/presentation/components/CustomerPortalLink.tsx +71 -0
  109. package/src/presentation/components/PricingTable.tsx +225 -0
  110. package/src/presentation/components/SubscriptionStatusBadge.tsx +102 -0
  111. package/src/presentation/context/billing-context.ts +30 -0
  112. package/src/presentation/hooks/useBilling.ts +3 -0
  113. package/src/presentation/hooks/useCancelSubscription.ts +47 -0
  114. package/src/presentation/hooks/useCheckoutButton.ts +49 -0
  115. package/src/presentation/hooks/useProducts.ts +76 -0
  116. package/src/presentation/navigation/navigation.strategy.ts +16 -0
  117. package/src/presentation/portal/TurnkeyBillingPage.tsx +144 -0
  118. package/src/presentation/portal/components/BillingLayout.tsx +163 -0
  119. package/src/presentation/portal/components/BillingPage.tsx +22 -0
  120. package/src/presentation/portal/components/BillingPortal.tsx +248 -0
  121. package/src/presentation/portal/components/InvoiceCard.tsx +275 -0
  122. package/src/presentation/portal/components/OverviewTab.tsx +137 -0
  123. package/src/presentation/portal/components/PaymentMethodsList.tsx +268 -0
  124. package/src/presentation/portal/components/PlanComparison.tsx +405 -0
  125. package/src/presentation/portal/components/PlanTab.tsx +197 -0
  126. package/src/presentation/portal/components/PortalButton.tsx +89 -0
  127. package/src/presentation/portal/components/PortalSpinner.tsx +30 -0
  128. package/src/presentation/portal/components/UsageCard.tsx +158 -0
  129. package/src/presentation/portal/components/index.ts +11 -0
  130. package/src/presentation/portal/constants.ts +44 -0
  131. package/src/presentation/portal/create-portal-client.ts +202 -0
  132. package/src/presentation/portal/hooks/index.ts +3 -0
  133. package/src/presentation/portal/hooks/useBilling.ts +240 -0
  134. package/src/presentation/portal/index.ts +97 -0
  135. package/src/presentation/portal/labels.tsx +218 -0
  136. package/src/presentation/portal/styles.ts +70 -0
  137. package/src/presentation/portal/types.ts +213 -0
  138. package/src/presentation/portal/utils/billing.ts +193 -0
  139. package/src/presentation/portal/utils/index.ts +27 -0
  140. package/src/presentation/portal/utils/plans.ts +71 -0
  141. package/src/presentation/theme/visual-tokens.ts +103 -0
  142. package/src/presentation/utils/cancellation-reason-catalog.ts +18 -0
  143. package/src/presentation/utils/checkout-button-visual.ts +50 -0
  144. package/src/presentation/utils/inline-error-visual.ts +21 -0
  145. package/src/presentation/utils/product-pricing.ts +33 -0
  146. package/src/presentation/utils/status-label.ts +23 -0
  147. package/src/presentation/utils/status-tone.ts +25 -0
  148. package/src/products.ts +20 -0
  149. package/src/react.tsx +63 -0
  150. package/src/server.ts +79 -0
package/package.json CHANGED
@@ -1,6 +1,124 @@
1
1
  {
2
2
  "name": "@umituz/web-payment",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Universal web payment & subscription billing engine with multi-provider architecture (Polar, Stripe, etc.)",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ },
18
+ "./core": {
19
+ "types": "./dist/core.d.ts",
20
+ "import": "./dist/core.mjs",
21
+ "require": "./dist/core.js"
22
+ },
23
+ "./products": {
24
+ "types": "./dist/products.d.ts",
25
+ "import": "./dist/products.mjs",
26
+ "require": "./dist/products.js"
27
+ },
28
+ "./react": {
29
+ "types": "./dist/react.d.ts",
30
+ "import": "./dist/react.mjs",
31
+ "require": "./dist/react.js"
32
+ },
33
+ "./portal": {
34
+ "types": "./dist/portal.d.ts",
35
+ "import": "./dist/portal.mjs",
36
+ "require": "./dist/portal.js"
37
+ },
38
+ "./server": {
39
+ "types": "./dist/server.d.ts",
40
+ "import": "./dist/server.mjs",
41
+ "require": "./dist/server.js"
42
+ },
43
+ "./http": {
44
+ "types": "./dist/http.d.ts",
45
+ "import": "./dist/http.mjs",
46
+ "require": "./dist/http.js"
47
+ },
48
+ "./memory": {
49
+ "types": "./dist/memory.d.ts",
50
+ "import": "./dist/memory.mjs",
51
+ "require": "./dist/memory.js"
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "src"
58
+ ],
59
+ "scripts": {
60
+ "build": "tsup src/index.ts src/core.ts src/products.ts src/react.tsx src/portal.ts src/server.ts src/http.ts src/memory.ts --format cjs,esm --dts --clean",
61
+ "dev": "tsup src/index.ts src/core.ts src/products.ts src/react.tsx src/portal.ts src/server.ts src/http.ts src/memory.ts --format cjs,esm --dts --watch",
62
+ "typecheck": "tsc --noEmit",
63
+ "lint": "eslint .",
64
+ "test": "vitest run",
65
+ "test:watch": "vitest",
66
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run build && npm test",
67
+ "version:patch": "npm version patch -m 'chore: release v%s'",
68
+ "version:minor": "npm version minor -m 'chore: release v%s'",
69
+ "version:major": "npm version major -m 'chore: release v%s'"
70
+ },
71
+ "engines": {
72
+ "node": ">=18.0.0"
73
+ },
74
+ "keywords": [
75
+ "payment",
76
+ "billing",
77
+ "subscription",
78
+ "polar",
79
+ "stripe",
80
+ "react",
81
+ "adapter",
82
+ "facade",
83
+ "repository"
84
+ ],
85
+ "author": "umituz",
86
+ "license": "MIT",
87
+ "repository": {
88
+ "type": "git",
89
+ "url": "git+https://github.com/umituz/web-payment.git"
90
+ },
91
+ "bugs": {
92
+ "url": "https://github.com/umituz/web-payment/issues"
93
+ },
94
+ "homepage": "https://github.com/umituz/web-payment#readme",
95
+ "peerDependencies": {
96
+ "react": ">=18.0.0"
97
+ },
98
+ "peerDependenciesMeta": {
99
+ "react": {
100
+ "optional": true
101
+ }
102
+ },
103
+ "dependencies": {
104
+ "lucide-react": "^0.577.0 || ^1.0.0"
105
+ },
106
+ "devDependencies": {
107
+ "@eslint/js": "^9.20.0",
108
+ "@testing-library/jest-dom": "^7.0.1",
109
+ "@testing-library/react": "^16.1.0",
110
+ "@types/node": "^26.6.6",
111
+ "@types/react": "^18.3.0",
112
+ "@types/react-dom": "^18.3.7",
113
+ "eslint": "^9.20.0",
114
+ "eslint-plugin-react-hooks": "^5.1.0",
115
+ "globals": "^15.14.0",
116
+ "jsdom": "^25.0.0",
117
+ "react": "^18.3.1",
118
+ "react-dom": "^18.3.1",
119
+ "tsup": "^8.0.0",
120
+ "typescript": "^5.0.0",
121
+ "typescript-eslint": "^8.23.0",
122
+ "vitest": "^3.0.0"
123
+ }
124
+ }
package/src/core.ts ADDED
@@ -0,0 +1,81 @@
1
+
2
+ export {
3
+ PAYMENT_PROVIDER,
4
+ PAYMENT_PROVIDER_VALUES,
5
+ isKnownPaymentProvider,
6
+ } from './domain/enums/payment-provider.enum';
7
+ export type { PaymentProvider } from './domain/enums/payment-provider.enum';
8
+
9
+ export {
10
+ SUBSCRIPTION_STATUS,
11
+ SUBSCRIPTION_STATUS_VALUES,
12
+ isKnownSubscriptionStatus,
13
+ } from './domain/enums/subscription-status.enum';
14
+ export type { SubscriptionStatusValue } from './domain/enums/subscription-status.enum';
15
+
16
+ export {
17
+ BILLING_CYCLE,
18
+ BILLING_CYCLE_VALUES,
19
+ isKnownBillingCycle,
20
+ parseBillingCycle,
21
+ } from './domain/enums/billing-cycle.enum';
22
+ export type { BillingCycle } from './domain/enums/billing-cycle.enum';
23
+
24
+ export { POLAR_AMOUNT_TYPE } from './domain/enums/amount-type.enum';
25
+ export type { AmountType } from './domain/enums/amount-type.enum';
26
+
27
+ export {
28
+ CANCELLATION_REASON,
29
+ CANCELLATION_REASON_VALUES,
30
+ } from './domain/entities/cancellation.entity';
31
+ export type { CancellationReason, CancelResult } from './domain/entities/cancellation.entity';
32
+
33
+ export { FREE_SUBSCRIPTION_STATUS, isActiveSubscription } from './domain/entities/subscription.entity';
34
+ export type { SubscriptionStatus } from './domain/entities/subscription.entity';
35
+ export type { CheckoutParams, CheckoutResult } from './domain/entities/checkout.entity';
36
+ export type { OrderItem } from './domain/entities/order.entity';
37
+ export type { SyncResult } from './domain/entities/sync.entity';
38
+ export type {
39
+ Product,
40
+ ProductPrice,
41
+ ProductBenefit,
42
+ ProductListResponse,
43
+ ProductListParams,
44
+ RecurringInterval,
45
+ PriceType,
46
+ } from './domain/entities/product.entity';
47
+
48
+ export type { BillingAdapter } from './domain/interfaces';
49
+ export { createMemoryBillingAdapter } from './infrastructure/services/memory/create-memory-adapter';
50
+ export type { MemoryAdapterConfig } from './infrastructure/services/memory/create-memory-adapter';
51
+
52
+ export { mapProduct, mapProductList } from './domain/mappers/product.mapper';
53
+ export type {
54
+ ProductDto,
55
+ ProductPriceDto,
56
+ ProductBenefitDto,
57
+ ProductListResponseDto,
58
+ } from './domain/mappers/product.mapper';
59
+
60
+ export {
61
+ minorUnitDivisor,
62
+ toMajorUnits,
63
+ toMinorUnits,
64
+ } from './domain/utils/minor-units';
65
+
66
+ export {
67
+ applyPercentDiscount,
68
+ normalizeCouponCode,
69
+ } from './domain/utils/percent-discount';
70
+
71
+ export { formatReceiptNumber } from './infrastructure/utils/receipt.util';
72
+
73
+ export {
74
+ buildCheckoutMetadata,
75
+ readCheckoutMetadata,
76
+ } from './infrastructure/utils/checkout-metadata';
77
+ export type {
78
+ BuildCheckoutMetadataInput,
79
+ CheckoutOrderMetadata,
80
+ } from './infrastructure/utils/checkout-metadata';
81
+
@@ -0,0 +1,28 @@
1
+ export const CANCELLATION_REASON = Object.freeze({
2
+ TOO_EXPENSIVE: 'too_expensive',
3
+ MISSING_FEATURES: 'missing_features',
4
+ SWITCHED_SERVICE: 'switched_service',
5
+ UNUSED: 'unused',
6
+ CUSTOMER_SERVICE: 'customer_service',
7
+ LOW_QUALITY: 'low_quality',
8
+ TOO_COMPLEX: 'too_complex',
9
+ OTHER: 'other',
10
+ } as const);
11
+
12
+ export const CANCELLATION_REASON_VALUES = Object.freeze([
13
+ CANCELLATION_REASON.TOO_EXPENSIVE,
14
+ CANCELLATION_REASON.MISSING_FEATURES,
15
+ CANCELLATION_REASON.SWITCHED_SERVICE,
16
+ CANCELLATION_REASON.UNUSED,
17
+ CANCELLATION_REASON.CUSTOMER_SERVICE,
18
+ CANCELLATION_REASON.LOW_QUALITY,
19
+ CANCELLATION_REASON.TOO_COMPLEX,
20
+ CANCELLATION_REASON.OTHER,
21
+ ] as const);
22
+
23
+ export type CancellationReason = (typeof CANCELLATION_REASON_VALUES)[number];
24
+
25
+ export interface CancelResult {
26
+ success: boolean;
27
+ endsAt?: string;
28
+ }
@@ -0,0 +1,21 @@
1
+ import type { BillingCycle } from '../enums/billing-cycle.enum';
2
+
3
+ export interface CheckoutParams {
4
+ productId?: string;
5
+ productKey?: string;
6
+ itemId?: string;
7
+ couponCode?: string;
8
+ planKey?: string;
9
+ billingCycle?: BillingCycle;
10
+ successUrl?: string;
11
+ userId?: string;
12
+ customerEmail?: string;
13
+ seats?: number;
14
+ metadata?: Record<string, string | number | boolean>;
15
+ }
16
+
17
+ export interface CheckoutResult {
18
+ url: string;
19
+ id: string;
20
+ }
21
+
@@ -0,0 +1,10 @@
1
+ export * from './subscription.entity';
2
+ export * from './checkout.entity';
3
+ export * from './order.entity';
4
+ export * from './cancellation.entity';
5
+ export * from './sync.entity';
6
+ export * from './product.entity';
7
+ export { SUBSCRIPTION_STATUS_VALUES, isKnownSubscriptionStatus } from '../enums/subscription-status.enum';
8
+ export type { SubscriptionStatusValue } from '../enums/subscription-status.enum';
9
+ export { BILLING_CYCLE, BILLING_CYCLE_VALUES, isKnownBillingCycle } from '../enums/billing-cycle.enum';
10
+ export type { BillingCycle } from '../enums/billing-cycle.enum';
@@ -0,0 +1,15 @@
1
+ export interface OrderItem {
2
+ id: string;
3
+ createdAt: string;
4
+ amount: number;
5
+ currency: string;
6
+ status: string;
7
+ paid: boolean;
8
+ productName?: string;
9
+ itemType?: string;
10
+ itemId?: string;
11
+ amountUsd?: number;
12
+ receiptNumber?: string;
13
+ invoiceUrl?: string;
14
+ }
15
+
@@ -0,0 +1,16 @@
1
+
2
+ export type {
3
+ Product,
4
+ ProductPrice,
5
+ ProductBenefit,
6
+ ProductListResponse,
7
+ ProductListParams,
8
+ } from '../mappers/product.mapper';
9
+
10
+ import type { BillingCycle } from '../enums/billing-cycle.enum';
11
+
12
+ export type RecurringInterval = 'day' | 'week' | 'month' | 'year';
13
+
14
+ export type PriceType = 'fixed' | 'custom' | 'free' | 'seat_based' | 'metered_unit';
15
+
16
+ export type { BillingCycle };
@@ -0,0 +1,24 @@
1
+ import type { SubscriptionStatusValue } from '../enums/subscription-status.enum';
2
+ import type { BillingCycle } from '../enums/billing-cycle.enum';
3
+
4
+ export interface SubscriptionStatus {
5
+ plan: string;
6
+ subscriptionId?: string;
7
+ subscriptionStatus: SubscriptionStatusValue;
8
+ cancelAtPeriodEnd?: boolean;
9
+ currentPeriodEnd?: string;
10
+ billingCycle?: BillingCycle;
11
+ providerCustomerId?: string;
12
+ }
13
+
14
+ export const FREE_SUBSCRIPTION_STATUS: SubscriptionStatus = Object.freeze({
15
+ plan: 'free',
16
+ subscriptionStatus: 'none',
17
+ }) as SubscriptionStatus;
18
+
19
+ export function isActiveSubscription(status: SubscriptionStatus): boolean {
20
+ return (
21
+ status.subscriptionStatus === 'active' ||
22
+ status.subscriptionStatus === 'trialing'
23
+ );
24
+ }
@@ -0,0 +1,4 @@
1
+ export interface SyncResult {
2
+ synced: boolean;
3
+ plan?: string;
4
+ }
@@ -0,0 +1,10 @@
1
+
2
+ export const POLAR_AMOUNT_TYPE = Object.freeze({
3
+ FIXED: 'fixed',
4
+ CUSTOM: 'custom',
5
+ FREE: 'free',
6
+ SEAT_BASED: 'seat_based',
7
+ METERED_UNIT: 'metered_unit',
8
+ } as const);
9
+
10
+ export type AmountType = (typeof POLAR_AMOUNT_TYPE)[keyof typeof POLAR_AMOUNT_TYPE];
@@ -0,0 +1,30 @@
1
+
2
+ export const BILLING_CYCLE = Object.freeze({
3
+ MONTHLY: 'monthly',
4
+ YEARLY: 'yearly',
5
+ } as const);
6
+
7
+ export const BILLING_CYCLE_VALUES = Object.freeze([
8
+ BILLING_CYCLE.MONTHLY,
9
+ BILLING_CYCLE.YEARLY,
10
+ ] as const);
11
+
12
+ export type BillingCycle = (typeof BILLING_CYCLE_VALUES)[number];
13
+
14
+ const CANONICAL_BILLING_CYCLES: ReadonlySet<string> = new Set(BILLING_CYCLE_VALUES);
15
+
16
+ const BILLING_CYCLE_ALIASES: ReadonlyMap<string, BillingCycle> = new Map([
17
+ [BILLING_CYCLE.MONTHLY, BILLING_CYCLE.MONTHLY],
18
+ ['month', BILLING_CYCLE.MONTHLY],
19
+ [BILLING_CYCLE.YEARLY, BILLING_CYCLE.YEARLY],
20
+ ['year', BILLING_CYCLE.YEARLY],
21
+ ]);
22
+
23
+ export function isKnownBillingCycle(value: unknown): value is BillingCycle {
24
+ return typeof value === 'string' && CANONICAL_BILLING_CYCLES.has(value.toLowerCase());
25
+ }
26
+
27
+ export function parseBillingCycle(value: unknown): BillingCycle | null {
28
+ if (typeof value !== 'string') return null;
29
+ return BILLING_CYCLE_ALIASES.get(value.toLowerCase()) ?? null;
30
+ }
@@ -0,0 +1,5 @@
1
+ export * from './subscription-status.enum';
2
+ export * from './billing-cycle.enum';
3
+ export * from './amount-type.enum';
4
+ export * from './payment-provider.enum';
5
+
@@ -0,0 +1,14 @@
1
+ export const PAYMENT_PROVIDER = {
2
+ POLAR: 'polar',
3
+ STRIPE: 'stripe',
4
+ } as const;
5
+
6
+ export const PAYMENT_PROVIDER_VALUES = Object.values(PAYMENT_PROVIDER);
7
+
8
+ export type PaymentProvider = (typeof PAYMENT_PROVIDER)[keyof typeof PAYMENT_PROVIDER];
9
+
10
+ const PAYMENT_PROVIDER_SET: ReadonlySet<string> = new Set<string>(PAYMENT_PROVIDER_VALUES);
11
+
12
+ export function isKnownPaymentProvider(value: unknown): value is PaymentProvider {
13
+ return typeof value === 'string' && PAYMENT_PROVIDER_SET.has(value);
14
+ }
@@ -0,0 +1,32 @@
1
+
2
+ export const SUBSCRIPTION_STATUS = {
3
+ ACTIVE: 'active',
4
+ CANCELED: 'canceled',
5
+ REVOKED: 'revoked',
6
+ TRIALING: 'trialing',
7
+ PAST_DUE: 'past_due',
8
+ INCOMPLETE: 'incomplete',
9
+ INCOMPLETE_EXPIRED: 'incomplete_expired',
10
+ UNPAID: 'unpaid',
11
+ NONE: 'none',
12
+ } as const;
13
+
14
+ export const SUBSCRIPTION_STATUS_VALUES = Object.freeze([
15
+ SUBSCRIPTION_STATUS.ACTIVE,
16
+ SUBSCRIPTION_STATUS.CANCELED,
17
+ SUBSCRIPTION_STATUS.REVOKED,
18
+ SUBSCRIPTION_STATUS.TRIALING,
19
+ SUBSCRIPTION_STATUS.PAST_DUE,
20
+ SUBSCRIPTION_STATUS.INCOMPLETE,
21
+ SUBSCRIPTION_STATUS.INCOMPLETE_EXPIRED,
22
+ SUBSCRIPTION_STATUS.UNPAID,
23
+ SUBSCRIPTION_STATUS.NONE,
24
+ ] as const);
25
+
26
+ export type SubscriptionStatusValue = (typeof SUBSCRIPTION_STATUS_VALUES)[number];
27
+
28
+ const SUBSCRIPTION_STATUS_SET: ReadonlySet<string> = new Set(SUBSCRIPTION_STATUS_VALUES);
29
+
30
+ export function isKnownSubscriptionStatus(value: unknown): value is SubscriptionStatusValue {
31
+ return typeof value === 'string' && SUBSCRIPTION_STATUS_SET.has(value.toLowerCase());
32
+ }
@@ -0,0 +1,5 @@
1
+ export * from './entities';
2
+ export * from './interfaces';
3
+ export * from './enums';
4
+ export * from './mappers/product.mapper';
5
+ export * from './utils/minor-units';
@@ -0,0 +1,18 @@
1
+ import type {
2
+ SubscriptionStatus,
3
+ CheckoutParams,
4
+ CheckoutResult,
5
+ OrderItem,
6
+ CancellationReason,
7
+ CancelResult,
8
+ SyncResult,
9
+ } from '../entities';
10
+
11
+ export interface BillingAdapter {
12
+ getStatus(userId: string): Promise<SubscriptionStatus>;
13
+ createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
14
+ syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
15
+ getBillingHistory(userId: string): Promise<OrderItem[]>;
16
+ cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
17
+ getPortalUrl(userId: string): Promise<string>;
18
+ }
@@ -0,0 +1 @@
1
+ export * from './billing-adapter.interface';
@@ -0,0 +1,176 @@
1
+
2
+ import type { AmountType } from '../enums/amount-type.enum';
3
+ import { parseBillingCycle, type BillingCycle } from '../enums/billing-cycle.enum';
4
+
5
+
6
+ export interface ProductDto {
7
+ id: string;
8
+ name: string;
9
+ description: string | null;
10
+ is_recurring: boolean;
11
+ recurring_interval: string | null;
12
+ is_archived: boolean;
13
+ organization_id: string;
14
+ prices: ProductPriceDto[];
15
+ benefits: ProductBenefitDto[];
16
+ metadata: Record<string, unknown>;
17
+ created_at: string;
18
+ modified_at: string | null;
19
+ }
20
+
21
+ export interface ProductPriceDto {
22
+ id: string;
23
+ type: string;
24
+ amount_type: string;
25
+ amount?: number;
26
+ currency: string;
27
+ recurring_interval?: string;
28
+ is_archived: boolean;
29
+ product_id: string;
30
+ legacy?: boolean;
31
+ }
32
+
33
+ export interface ProductBenefitDto {
34
+ id: string;
35
+ type: string;
36
+ description: string | null;
37
+ properties: Record<string, unknown>;
38
+ }
39
+
40
+ export interface ProductListResponseDto {
41
+ items: ProductDto[];
42
+ pagination: {
43
+ total_count: number;
44
+ max_page: number;
45
+ };
46
+ }
47
+
48
+
49
+ export interface ProductPrice {
50
+ id: string;
51
+ type: AmountType;
52
+ amountType: AmountType;
53
+ amount?: number;
54
+ currency: string;
55
+ recurringInterval?: BillingCycle;
56
+ isArchived: boolean;
57
+ productId: string;
58
+ legacy?: boolean;
59
+ }
60
+
61
+ export interface ProductBenefit {
62
+ id: string;
63
+ type: string;
64
+ description: string | null;
65
+ properties: Record<string, unknown>;
66
+ }
67
+
68
+ export interface Product {
69
+ id: string;
70
+ name: string;
71
+ description: string | null;
72
+ isRecurring: boolean;
73
+ recurringInterval: BillingCycle | null;
74
+ isArchived: boolean;
75
+ organizationId: string;
76
+ prices: ProductPrice[];
77
+ benefits: ProductBenefit[];
78
+ metadata: Record<string, unknown>;
79
+ createdAt: string;
80
+ modifiedAt: string | null;
81
+ }
82
+
83
+ export interface ProductListResponse {
84
+ items: Product[];
85
+ pagination: {
86
+ totalCount: number;
87
+ maxPage: number;
88
+ };
89
+ }
90
+
91
+ export interface ProductListParams {
92
+ organizationId?: string;
93
+ isRecurring?: boolean;
94
+ page?: number;
95
+ limit?: number;
96
+ }
97
+
98
+
99
+ const KNOWN_AMOUNT_TYPES: ReadonlySet<string> = new Set([
100
+ 'fixed',
101
+ 'custom',
102
+ 'free',
103
+ 'seat_based',
104
+ 'metered_unit',
105
+ ]);
106
+
107
+ function toAmountType(raw: string | undefined): AmountType {
108
+ if (raw && KNOWN_AMOUNT_TYPES.has(raw)) return raw as AmountType;
109
+ return 'fixed';
110
+ }
111
+
112
+ function toBillingCycle(interval: string | null | undefined): BillingCycle | null {
113
+ return parseBillingCycle(interval);
114
+ }
115
+
116
+ function asRecord(value: unknown): Record<string, unknown> {
117
+ return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
118
+ }
119
+
120
+ function toPrice(dto: ProductPriceDto): ProductPrice {
121
+ return {
122
+ id: String(dto.id),
123
+ type: toAmountType(dto.type),
124
+ amountType: toAmountType(dto.amount_type),
125
+ amount: typeof dto.amount === 'number' ? dto.amount : undefined,
126
+ currency: String(dto.currency),
127
+ recurringInterval: toBillingCycle(dto.recurring_interval) ?? undefined,
128
+ isArchived: Boolean(dto.is_archived),
129
+ productId: String(dto.product_id),
130
+ legacy: Boolean(dto.legacy),
131
+ };
132
+ }
133
+
134
+ function toBenefit(dto: ProductBenefitDto): ProductBenefit {
135
+ return {
136
+ id: String(dto.id),
137
+ type: String(dto.type),
138
+ description: dto.description ?? null,
139
+ properties: asRecord(dto.properties),
140
+ };
141
+ }
142
+
143
+ export function mapProduct(dto: ProductDto | null | undefined): Product {
144
+ if (!dto || typeof dto !== 'object') {
145
+ throw new TypeError('mapProduct: expected a product DTO object');
146
+ }
147
+ return {
148
+ id: String(dto.id),
149
+ name: String(dto.name),
150
+ description: dto.description ?? null,
151
+ isRecurring: Boolean(dto.is_recurring),
152
+ recurringInterval: toBillingCycle(dto.recurring_interval),
153
+ isArchived: Boolean(dto.is_archived),
154
+ organizationId: String(dto.organization_id),
155
+ prices: Array.isArray(dto.prices) ? dto.prices.map(toPrice) : [],
156
+ benefits: Array.isArray(dto.benefits) ? dto.benefits.map(toBenefit) : [],
157
+ metadata: asRecord(dto.metadata),
158
+ createdAt: String(dto.created_at),
159
+ modifiedAt: dto.modified_at ?? null,
160
+ };
161
+ }
162
+
163
+ export function mapProductList(
164
+ dto: ProductListResponseDto | null | undefined,
165
+ ): ProductListResponse {
166
+ if (!dto || typeof dto !== 'object') {
167
+ return { items: [], pagination: { totalCount: 0, maxPage: 1 } };
168
+ }
169
+ return {
170
+ items: Array.isArray(dto.items) ? dto.items.map(mapProduct) : [],
171
+ pagination: {
172
+ totalCount: Number(dto.pagination?.total_count ?? 0),
173
+ maxPage: Number(dto.pagination?.max_page ?? 1),
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,20 @@
1
+
2
+ const MINOR_UNIT_DIVISOR = 100;
3
+
4
+ const ZERO_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set([
5
+ 'JPY', 'KRW', 'VND', 'CLP', 'PYG', 'XAF', 'XOF', 'BIF', 'DJF', 'GNF', 'XPF',
6
+ ]);
7
+
8
+ export function minorUnitDivisor(currency: string): number {
9
+ return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase())
10
+ ? 1
11
+ : MINOR_UNIT_DIVISOR;
12
+ }
13
+
14
+ export function toMajorUnits(amount: number, currency: string): number {
15
+ return amount / minorUnitDivisor(currency);
16
+ }
17
+
18
+ export function toMinorUnits(amount: number, currency: string): number {
19
+ return Math.round(amount * minorUnitDivisor(currency));
20
+ }
@@ -0,0 +1,17 @@
1
+
2
+ export function normalizeCouponCode(code: unknown): string | null {
3
+ if (typeof code !== 'string') return null;
4
+ const normalized = code.trim().toUpperCase();
5
+ return normalized.length > 0 ? normalized : null;
6
+ }
7
+
8
+ export function applyPercentDiscount(
9
+ baseAmount: number,
10
+ percent: number,
11
+ minimumAmount: number,
12
+ ): number {
13
+ if (!Number.isFinite(baseAmount) || baseAmount <= 0) return 0;
14
+ if (!Number.isFinite(percent) || percent <= 0) return Math.round(baseAmount);
15
+ const discounted = Math.round(baseAmount * (1 - percent / 100));
16
+ return Math.max(minimumAmount, discounted);
17
+ }