@misterhomer1992/payment-manager 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 (89) hide show
  1. package/.editorconfig +7 -0
  2. package/.prettierrc +5 -0
  3. package/README.md +181 -0
  4. package/USAGE.md +400 -0
  5. package/dist/collections/payments.d.ts +6 -0
  6. package/dist/collections/payments.js +60 -0
  7. package/dist/collections/plans.d.ts +6 -0
  8. package/dist/collections/plans.js +50 -0
  9. package/dist/collections/subscriptions.d.ts +6 -0
  10. package/dist/collections/subscriptions.js +47 -0
  11. package/dist/collections/tokenPacks.d.ts +6 -0
  12. package/dist/collections/tokenPacks.js +42 -0
  13. package/dist/collections/userTokenBalances.d.ts +4 -0
  14. package/dist/collections/userTokenBalances.js +41 -0
  15. package/dist/config.d.ts +20 -0
  16. package/dist/config.js +14 -0
  17. package/dist/constants/defaultPlans.d.ts +2 -0
  18. package/dist/constants/defaultPlans.js +37 -0
  19. package/dist/index.d.ts +51 -0
  20. package/dist/index.js +88 -0
  21. package/dist/services/paymentService.d.ts +8 -0
  22. package/dist/services/paymentService.js +173 -0
  23. package/dist/services/planService.d.ts +7 -0
  24. package/dist/services/planService.js +69 -0
  25. package/dist/services/subscriptionService.d.ts +13 -0
  26. package/dist/services/subscriptionService.js +183 -0
  27. package/dist/services/tokenPackService.d.ts +15 -0
  28. package/dist/services/tokenPackService.js +80 -0
  29. package/dist/services/webhookService.d.ts +3 -0
  30. package/dist/services/webhookService.js +210 -0
  31. package/dist/types/errors.d.ts +31 -0
  32. package/dist/types/errors.js +74 -0
  33. package/dist/types/index.d.ts +7 -0
  34. package/dist/types/index.js +14 -0
  35. package/dist/types/orderReference.d.ts +18 -0
  36. package/dist/types/orderReference.js +2 -0
  37. package/dist/types/payment.d.ts +14 -0
  38. package/dist/types/payment.js +2 -0
  39. package/dist/types/plan.d.ts +10 -0
  40. package/dist/types/plan.js +2 -0
  41. package/dist/types/subscription.d.ts +12 -0
  42. package/dist/types/subscription.js +2 -0
  43. package/dist/types/tokenPack.d.ts +15 -0
  44. package/dist/types/tokenPack.js +2 -0
  45. package/dist/types/webhook.d.ts +32 -0
  46. package/dist/types/webhook.js +2 -0
  47. package/dist/utils/gracePeriod.d.ts +3 -0
  48. package/dist/utils/gracePeriod.js +19 -0
  49. package/dist/utils/orderReference.d.ts +3 -0
  50. package/dist/utils/orderReference.js +52 -0
  51. package/dist/utils/proration.d.ts +10 -0
  52. package/dist/utils/proration.js +32 -0
  53. package/dist/utils/webhookResponse.d.ts +2 -0
  54. package/dist/utils/webhookResponse.js +10 -0
  55. package/jest.config.ts +11 -0
  56. package/package.json +29 -0
  57. package/src/collections/payments.ts +61 -0
  58. package/src/collections/plans.ts +53 -0
  59. package/src/collections/subscriptions.ts +52 -0
  60. package/src/collections/tokenPacks.ts +42 -0
  61. package/src/collections/userTokenBalances.ts +46 -0
  62. package/src/config.ts +34 -0
  63. package/src/index.ts +129 -0
  64. package/src/services/deactivationCheckHandler.test.ts +248 -0
  65. package/src/services/paymentCheckHandler.test.ts +384 -0
  66. package/src/services/paymentService.ts +166 -0
  67. package/src/services/planService.ts +46 -0
  68. package/src/services/subscriptionService.ts +183 -0
  69. package/src/services/tokenPackService.ts +54 -0
  70. package/src/services/webhookService.ts +217 -0
  71. package/src/types/errors.ts +72 -0
  72. package/src/types/index.ts +18 -0
  73. package/src/types/orderReference.ts +19 -0
  74. package/src/types/payment.ts +14 -0
  75. package/src/types/plan.ts +10 -0
  76. package/src/types/subscription.ts +12 -0
  77. package/src/types/tokenPack.ts +16 -0
  78. package/src/types/webhook.ts +34 -0
  79. package/src/utils/gracePeriod.test.ts +46 -0
  80. package/src/utils/gracePeriod.ts +19 -0
  81. package/src/utils/orderReference.test.ts +149 -0
  82. package/src/utils/orderReference.ts +57 -0
  83. package/src/utils/proration.test.ts +133 -0
  84. package/src/utils/proration.ts +50 -0
  85. package/src/utils/webhookResponse.test.ts +57 -0
  86. package/src/utils/webhookResponse.ts +14 -0
  87. package/tasks/prd-payment-manager-library-product-requirements-document.md +447 -0
  88. package/tasks/prd.json +336 -0
  89. package/tsconfig.json +15 -0
package/.editorconfig ADDED
@@ -0,0 +1,7 @@
1
+ root = true
2
+ [*]
3
+ indent_style = tab
4
+ indent_size = 4
5
+ insert_final_newline = true
6
+ trim_trailing_whitespace = true
7
+ max_line_length = 120
package/.prettierrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "tabWidth": 4,
3
+ "semi": true,
4
+ "singleQuote": true
5
+ }
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # payment-manager
2
+
3
+ A TypeScript library for managing subscriptions, one-time token purchases, and payment webhooks with [WayForPay](https://wayforpay.com/) and [Cloud Firestore](https://firebase.google.com/docs/firestore).
4
+
5
+ Framework-agnostic — wire it into Express, Fastify, Cloud Functions, or anything else.
6
+
7
+ ## Features
8
+
9
+ - Subscription lifecycle: create, upgrade/downgrade (with proration), cancel, expire
10
+ - One-time token pack purchases
11
+ - WayForPay webhook handling (payment confirmation + recurrent billing)
12
+ - Cron-compatible expiration checks with grace periods
13
+ - Payment URL caching (5-minute deduplication window)
14
+ - Typed error classes for every failure scenario
15
+ - Optional structured logging
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install payment-manager firebase-admin
21
+ ```
22
+
23
+ `firebase-admin` (>=12.0.0) is a peer dependency.
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import * as admin from 'firebase-admin';
29
+ import { init } from 'payment-manager';
30
+
31
+ admin.initializeApp();
32
+
33
+ const pm = init({
34
+ firestore: admin.firestore(),
35
+ wayforpay: {
36
+ merchantAccount: 'your_merchant_account',
37
+ merchantSecretKey: 'your_merchant_secret_key',
38
+ merchantDomainName: 'example.com',
39
+ },
40
+ platform: 'my-app',
41
+ webhookSecret: 'your-webhook-secret',
42
+ logger: console,
43
+ });
44
+
45
+ await pm.plans.seedDefaultPlans([
46
+ { id: 'basic-monthly', name: 'Basic', amount: 100, regularMode: 'monthly', isActive: true },
47
+ { id: 'pro-monthly', name: 'Pro', amount: 300, regularMode: 'monthly', isActive: true },
48
+ ]);
49
+
50
+ const { paymentUrl } = await pm.subscriptions.create({
51
+ userId: 'user-123',
52
+ planId: 'basic-monthly',
53
+ currency: 'UAH',
54
+ productName: 'My App Subscription',
55
+ });
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ ```typescript
61
+ interface PaymentManagerConfig {
62
+ firestore: Firestore;
63
+ wayforpay: {
64
+ merchantAccount: string;
65
+ merchantSecretKey: string;
66
+ merchantDomainName: string;
67
+ };
68
+ platform: string; // included in order references
69
+ webhookSecret?: string; // validates x-secret-key header on webhooks
70
+ logger?: { // any object with info/warn/error (console, pino, winston)
71
+ info: (message: string, ...args: unknown[]) => void;
72
+ warn: (message: string, ...args: unknown[]) => void;
73
+ error: (message: string, ...args: unknown[]) => void;
74
+ };
75
+ }
76
+ ```
77
+
78
+ ## API
79
+
80
+ `init()` returns a `PaymentManager` object with five namespaces.
81
+
82
+ ### `pm.plans`
83
+
84
+ | Method | Signature | Description |
85
+ |---|---|---|
86
+ | `seedDefaultPlans` | `(plans: PlanInput[]) => Promise<void>` | Upsert an array of plans by ID |
87
+ | `create` | `(plan: PlanInput) => Promise<void>` | Create a single plan |
88
+ | `getById` | `(planId: string) => Promise<SubscriptionPlanEntity \| null>` | Get a plan by ID |
89
+ | `getAll` | `() => Promise<SubscriptionPlanEntity[]>` | List all plans |
90
+ | `update` | `(planId: string, data: Partial<PlanInput>) => Promise<void>` | Update plan fields |
91
+ | `deactivate` | `(planId: string) => Promise<void>` | Mark a plan as inactive |
92
+
93
+ > `PlanInput` = `Omit<SubscriptionPlanEntity, 'createdAt' | 'updatedAt'>`
94
+
95
+ ### `pm.subscriptions`
96
+
97
+ | Method | Signature | Description |
98
+ |---|---|---|
99
+ | `create` | `(params: { userId, planId, currency, productName }) => Promise<{ paymentUrl }>` | Create a subscription and return a WayForPay payment URL |
100
+ | `getActiveByUserId` | `(userId: string) => Promise<SubscriptionEntity \| null>` | Get the active subscription for a user |
101
+ | `changePlan` | `(userId: string, newPlanId: string) => Promise<void>` | Change plan (upgrade = immediate proration, downgrade = next billing cycle) |
102
+ | `deactivate` | `(userId: string) => Promise<void>` | Cancel subscription via WayForPay |
103
+ | `expire` | `(userId: string) => Promise<void>` | Manually expire a subscription |
104
+
105
+ ### `pm.tokenPacks`
106
+
107
+ | Method | Signature | Description |
108
+ |---|---|---|
109
+ | `create` | `(pack: TokenPackInput) => Promise<string>` | Create a token pack, returns the generated ID |
110
+ | `getAll` | `() => Promise<TokenPackEntity[]>` | List all token packs |
111
+ | `activate` | `(packId: string) => Promise<void>` | Activate a token pack |
112
+ | `deactivate` | `(packId: string) => Promise<void>` | Deactivate a token pack |
113
+ | `remove` | `(packId: string) => Promise<void>` | Delete a token pack |
114
+ | `buyExtraTokens` | `(params: { userId, packId, currency, productName }) => Promise<{ paymentUrl }>` | Purchase tokens, returns a one-time payment URL |
115
+
116
+ > `TokenPackInput` = `Omit<TokenPackEntity, 'id' | 'createdAt' | 'updatedAt'>`
117
+
118
+ ### `pm.userTokens`
119
+
120
+ | Method | Signature | Description |
121
+ |---|---|---|
122
+ | `getBalance` | `(userId: string) => Promise<UserTokenBalance>` | Get user's token balance (returns zero-balance default if no record exists) |
123
+
124
+ ### `pm.webhooks`
125
+
126
+ | Method | Signature | Description |
127
+ |---|---|---|
128
+ | `handlePaymentCheck` | `(reqBody, headers) => Promise<WebhookResponse>` | Handle WayForPay `PAYMENT_CHECK` callback |
129
+ | `handleSubscriptionDeactivationCheck` | `(headers) => Promise<DeactivationResult>` | Expire overdue subscriptions and apply pending downgrades (call from a cron job) |
130
+
131
+ ## Errors
132
+
133
+ All errors extend `PaymentManagerError` with a `code` property:
134
+
135
+ ```typescript
136
+ import { PaymentManagerError } from 'payment-manager';
137
+
138
+ try {
139
+ await pm.subscriptions.create({ userId, planId, currency, productName });
140
+ } catch (error) {
141
+ if (error instanceof PaymentManagerError) {
142
+ console.error(error.code, error.message);
143
+ }
144
+ }
145
+ ```
146
+
147
+ | Error | Code |
148
+ |---|---|
149
+ | `PlanNotFoundError` | `PLAN_NOT_FOUND` |
150
+ | `PlanInactiveError` | `PLAN_INACTIVE` |
151
+ | `SubscriptionNotFoundError` | `SUBSCRIPTION_NOT_FOUND` |
152
+ | `SubscriptionAlreadyActiveError` | `SUBSCRIPTION_ALREADY_ACTIVE` |
153
+ | `TokenPackNotFoundError` | `TOKEN_PACK_NOT_FOUND` |
154
+ | `TokenPackInactiveError` | `TOKEN_PACK_INACTIVE` |
155
+ | `InvalidWebhookSecretError` | `INVALID_WEBHOOK_SECRET` |
156
+ | `PaymentProviderError` | `PAYMENT_PROVIDER_ERROR` |
157
+ | `InvalidOrderReferenceError` | `INVALID_ORDER_REFERENCE` |
158
+
159
+ ## Grace Periods
160
+
161
+ The deactivation check uses grace periods before expiring subscriptions:
162
+
163
+ | Billing Cycle | Grace Period |
164
+ |---|---|
165
+ | daily | 6 hours |
166
+ | monthly | 1 day |
167
+ | yearly | 1 day |
168
+
169
+ ## Firestore Collections
170
+
171
+ | Collection | Description |
172
+ |---|---|
173
+ | `subscriptionPlans` | Plan definitions |
174
+ | `subscriptions` | User subscription records |
175
+ | `payments` | Payment transactions |
176
+ | `tokenPacks` | Token pack definitions |
177
+ | `userTokenBalances` | Per-user token balances |
178
+
179
+ ## License
180
+
181
+ ISC
package/USAGE.md ADDED
@@ -0,0 +1,400 @@
1
+ # Payment Manager
2
+
3
+ A standalone TypeScript library for managing subscriptions, one-time token pack purchases, and payment webhooks using **WayForPay** as the payment provider and **Firestore** as the database.
4
+
5
+ The library is framework-agnostic — it exposes plain async functions that you wire into your own HTTP routes (Express, Fastify, Cloud Functions, etc.).
6
+
7
+ ## Key Features
8
+
9
+ - Full subscription lifecycle: create, upgrade/downgrade with proration, cancel, expire
10
+ - One-time token pack purchases (extra credits beyond subscription limits)
11
+ - WayForPay webhook handling for payment confirmation and recurrent billing
12
+ - Cron-compatible subscription expiration check with configurable grace periods
13
+ - Payment URL caching (avoids duplicate WayForPay API calls within a 5-minute window)
14
+ - Typed custom error classes for every failure scenario
15
+ - Optional structured logging
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install payment-manager
21
+ ```
22
+
23
+ ### Peer Dependencies
24
+
25
+ The library requires `firebase-admin` (>=12.0.0) as a peer dependency. Install it in your project if you haven't already:
26
+
27
+ ```bash
28
+ npm install firebase-admin
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import * as admin from 'firebase-admin';
35
+ import { init } from 'payment-manager';
36
+
37
+ admin.initializeApp();
38
+ const firestore = admin.firestore();
39
+
40
+ const pm = init({
41
+ firestore,
42
+ wayforpay: {
43
+ merchantAccount: 'your_merchant_account',
44
+ merchantSecretKey: 'your_merchant_secret_key',
45
+ merchantDomainName: 'yourdomain.com',
46
+ },
47
+ platform: 'my-app',
48
+ webhookSecret: 'your-webhook-secret', // optional
49
+ logger: console, // optional
50
+ });
51
+
52
+ // Seed your plans into Firestore
53
+ await pm.plans.seedDefaultPlans([
54
+ { id: 'basic-monthly', name: 'Basic Monthly', amount: 100, regularMode: 'monthly', isActive: true },
55
+ { id: 'pro-monthly', name: 'Pro Monthly', amount: 300, regularMode: 'monthly', isActive: true },
56
+ ]);
57
+
58
+ // Create a subscription (returns a WayForPay payment URL)
59
+ const { paymentUrl } = await pm.subscriptions.create({
60
+ userId: 'user-123',
61
+ planId: 'basic-monthly',
62
+ currency: 'UAH',
63
+ productName: 'My App Subscription',
64
+ });
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Pass a `PaymentManagerConfig` object to `init()`:
70
+
71
+ ```typescript
72
+ interface PaymentManagerConfig {
73
+ /** Firestore instance from firebase-admin */
74
+ firestore: Firestore;
75
+
76
+ /** WayForPay merchant credentials */
77
+ wayforpay: {
78
+ merchantAccount: string;
79
+ merchantSecretKey: string;
80
+ merchantDomainName: string;
81
+ };
82
+
83
+ /** Platform identifier included in order references */
84
+ platform: string;
85
+
86
+ /**
87
+ * Secret for validating incoming webhook requests (x-secret-key header).
88
+ * Falls back to process.env.NOTIFY_TELEGRAM_USER_SECRET if not provided.
89
+ */
90
+ webhookSecret?: string;
91
+
92
+ /**
93
+ * Optional logger. Any object with info/warn/error methods works
94
+ * (e.g. console, winston, pino).
95
+ */
96
+ logger?: {
97
+ info: (message: string, ...args: unknown[]) => void;
98
+ warn: (message: string, ...args: unknown[]) => void;
99
+ error: (message: string, ...args: unknown[]) => void;
100
+ };
101
+ }
102
+ ```
103
+
104
+ Calling any method before `init()` throws:
105
+ ```
106
+ Error: PaymentManager is not initialized. Call init(config) first.
107
+ ```
108
+
109
+ ## API Reference
110
+
111
+ The `init()` function returns a `PaymentManager` object with five namespaces:
112
+
113
+ ### `pm.plans` — Subscription Plan Management
114
+
115
+ ```typescript
116
+ // Seed your plans into Firestore (upserts by ID)
117
+ await pm.plans.seedDefaultPlans([
118
+ { id: 'basic-monthly', name: 'Basic Monthly', amount: 100, regularMode: 'monthly', isActive: true },
119
+ { id: 'pro-monthly', name: 'Pro Monthly', amount: 300, regularMode: 'monthly', isActive: true },
120
+ ]);
121
+
122
+ // CRUD operations
123
+ await pm.plans.create({ id: 'enterprise', name: 'Enterprise', amount: 500, regularMode: 'monthly', isActive: true });
124
+ const plan = await pm.plans.getById('basic-monthly');
125
+ const allPlans = await pm.plans.getAll();
126
+ await pm.plans.update('enterprise', { amount: 450 });
127
+ await pm.plans.deactivate('enterprise');
128
+ ```
129
+
130
+ ### `pm.subscriptions` — Subscription Lifecycle
131
+
132
+ ```typescript
133
+ // Create a new subscription (cancels any existing active subscription first)
134
+ const { paymentUrl } = await pm.subscriptions.create({
135
+ userId: 'user-123',
136
+ planId: 'pro-monthly',
137
+ currency: 'UAH',
138
+ productName: 'My App Pro',
139
+ });
140
+
141
+ // Get current active subscription
142
+ const sub = await pm.subscriptions.getActiveByUserId('user-123');
143
+
144
+ // Change plan (upgrade = immediate proration, downgrade = applied at next billing cycle)
145
+ await pm.subscriptions.changePlan('user-123', 'basic-monthly');
146
+
147
+ // Cancel subscription (calls WayForPay cancelRegularPurchase)
148
+ await pm.subscriptions.deactivate('user-123');
149
+
150
+ // Manually expire a subscription
151
+ await pm.subscriptions.expire('user-123');
152
+ ```
153
+
154
+ **Plan change behavior:**
155
+ - **Upgrade** (higher price): The remaining value of the current plan is prorated and converted into days on the new plan. The change is applied immediately.
156
+ - **Downgrade** (lower price): The new plan is stored as `pendingPlanId` and applied automatically at the next billing cycle.
157
+
158
+ ### `pm.tokenPacks` — One-Time Token Purchases
159
+
160
+ ```typescript
161
+ // Create and manage token packs
162
+ const packId = await pm.tokenPacks.create({ name: '100 Tokens', tokenAmount: 100, price: 50, isActive: true });
163
+ const allPacks = await pm.tokenPacks.getAll();
164
+ await pm.tokenPacks.deactivate(packId);
165
+ await pm.tokenPacks.activate(packId);
166
+ await pm.tokenPacks.remove(packId);
167
+
168
+ // Purchase tokens (returns a one-time payment URL)
169
+ const { paymentUrl } = await pm.tokenPacks.buyExtraTokens({
170
+ userId: 'user-123',
171
+ packId: 'pack-100',
172
+ currency: 'UAH',
173
+ productName: '100 Extra Tokens',
174
+ });
175
+ ```
176
+
177
+ ### `pm.userTokens` — Token Balance
178
+
179
+ ```typescript
180
+ // Get user's token balance (returns zero-balance default if no record exists)
181
+ const balance = await pm.userTokens.getBalance('user-123');
182
+ // balance: { userId, balance, totalPurchased, updatedAt }
183
+ ```
184
+
185
+ ### `pm.webhooks` — Webhook Handlers
186
+
187
+ ```typescript
188
+ // Handle WayForPay PAYMENT_CHECK webhook
189
+ const response = await pm.webhooks.handlePaymentCheck(req.body, req.headers);
190
+ // response: { orderReference, status: 'accept' | 'reject', time, signature }
191
+
192
+ // Handle subscription expiration check (call from a cron job)
193
+ const result = await pm.webhooks.handleSubscriptionDeactivationCheck(req.headers);
194
+ // result: { expired: string[], downgraded: string[] }
195
+ ```
196
+
197
+ ## Wiring into Your Server
198
+
199
+ ### Express Example
200
+
201
+ ```typescript
202
+ import express from 'express';
203
+ import * as admin from 'firebase-admin';
204
+ import { init } from 'payment-manager';
205
+
206
+ admin.initializeApp();
207
+ const app = express();
208
+ app.use(express.json());
209
+
210
+ const pm = init({
211
+ firestore: admin.firestore(),
212
+ wayforpay: {
213
+ merchantAccount: process.env.WFP_MERCHANT_ACCOUNT!,
214
+ merchantSecretKey: process.env.WFP_MERCHANT_SECRET!,
215
+ merchantDomainName: process.env.WFP_MERCHANT_DOMAIN!,
216
+ },
217
+ platform: 'my-app',
218
+ webhookSecret: process.env.WEBHOOK_SECRET,
219
+ logger: console,
220
+ });
221
+
222
+ // --- Plans ---
223
+ app.post('/api/plans/seed', async (req, res) => {
224
+ await pm.plans.seedDefaultPlans([
225
+ { id: 'basic-monthly', name: 'Basic Monthly', amount: 100, regularMode: 'monthly', isActive: true },
226
+ { id: 'pro-monthly', name: 'Pro Monthly', amount: 300, regularMode: 'monthly', isActive: true },
227
+ ]);
228
+ res.json({ ok: true });
229
+ });
230
+
231
+ // --- Subscriptions ---
232
+ app.post('/api/subscriptions', async (req, res) => {
233
+ const { userId, planId, currency, productName } = req.body;
234
+ const result = await pm.subscriptions.create({ userId, planId, currency, productName });
235
+ res.json(result);
236
+ });
237
+
238
+ app.post('/api/subscriptions/change-plan', async (req, res) => {
239
+ const { userId, newPlanId } = req.body;
240
+ await pm.subscriptions.changePlan(userId, newPlanId);
241
+ res.json({ ok: true });
242
+ });
243
+
244
+ app.post('/api/subscriptions/cancel', async (req, res) => {
245
+ await pm.subscriptions.deactivate(req.body.userId);
246
+ res.json({ ok: true });
247
+ });
248
+
249
+ // --- Token Packs ---
250
+ app.post('/api/tokens/buy', async (req, res) => {
251
+ const { userId, packId, currency, productName } = req.body;
252
+ const result = await pm.tokenPacks.buyExtraTokens({ userId, packId, currency, productName });
253
+ res.json(result);
254
+ });
255
+
256
+ app.get('/api/tokens/balance/:userId', async (req, res) => {
257
+ const balance = await pm.userTokens.getBalance(req.params.userId);
258
+ res.json(balance);
259
+ });
260
+
261
+ // --- Webhooks ---
262
+ app.post('/api/webhooks/payment', async (req, res) => {
263
+ const response = await pm.webhooks.handlePaymentCheck(req.body, req.headers as Record<string, string>);
264
+ res.json(response);
265
+ });
266
+
267
+ // --- Cron (subscription expiration check) ---
268
+ app.post('/api/cron/deactivation-check', async (req, res) => {
269
+ const result = await pm.webhooks.handleSubscriptionDeactivationCheck(req.headers as Record<string, string>);
270
+ res.json(result);
271
+ });
272
+
273
+ app.listen(3000);
274
+ ```
275
+
276
+ ### Firebase Cloud Functions Example
277
+
278
+ ```typescript
279
+ import * as admin from 'firebase-admin';
280
+ import { onRequest } from 'firebase-functions/v2/https';
281
+ import { onSchedule } from 'firebase-functions/v2/scheduler';
282
+ import { init } from 'payment-manager';
283
+
284
+ admin.initializeApp();
285
+
286
+ const pm = init({
287
+ firestore: admin.firestore(),
288
+ wayforpay: {
289
+ merchantAccount: process.env.WFP_MERCHANT_ACCOUNT!,
290
+ merchantSecretKey: process.env.WFP_MERCHANT_SECRET!,
291
+ merchantDomainName: process.env.WFP_MERCHANT_DOMAIN!,
292
+ },
293
+ platform: 'my-app',
294
+ webhookSecret: process.env.WEBHOOK_SECRET,
295
+ });
296
+
297
+ export const createSubscription = onRequest(async (req, res) => {
298
+ const { userId, planId, currency, productName } = req.body;
299
+ const result = await pm.subscriptions.create({ userId, planId, currency, productName });
300
+ res.json(result);
301
+ });
302
+
303
+ export const wayforpayWebhook = onRequest(async (req, res) => {
304
+ const response = await pm.webhooks.handlePaymentCheck(req.body, req.headers as Record<string, string>);
305
+ res.json(response);
306
+ });
307
+
308
+ export const deactivationCheck = onSchedule('every 1 hours', async () => {
309
+ const headers = { 'x-secret-key': process.env.WEBHOOK_SECRET };
310
+ await pm.webhooks.handleSubscriptionDeactivationCheck(headers as Record<string, string>);
311
+ });
312
+ ```
313
+
314
+ ## Firestore Collections
315
+
316
+ The library creates and manages the following Firestore collections:
317
+
318
+ | Collection | Description |
319
+ |---|---|
320
+ | `subscriptionPlans` | Subscription plan definitions |
321
+ | `subscriptions` | User subscription records |
322
+ | `payments` | Payment transaction records |
323
+ | `tokenPacks` | Token pack definitions |
324
+ | `userTokenBalances` | Per-user token balance |
325
+
326
+ ## Seeding Plans
327
+
328
+ `pm.plans.seedDefaultPlans(plans)` accepts an array of plan objects and upserts them by ID. Define whatever plans your application needs:
329
+
330
+ ```typescript
331
+ await pm.plans.seedDefaultPlans([
332
+ { id: 'basic-monthly', name: 'Basic', amount: 100, regularMode: 'monthly', isActive: true },
333
+ { id: 'pro-monthly', name: 'Pro', amount: 300, regularMode: 'monthly', isActive: true },
334
+ { id: 'enterprise-yearly', name: 'Enterprise', amount: 3000, regularMode: 'yearly', description: 'Annual enterprise plan', isActive: true },
335
+ ]);
336
+ ```
337
+
338
+ ## Error Handling
339
+
340
+ All errors extend `PaymentManagerError` with a `code` property for programmatic handling:
341
+
342
+ ```typescript
343
+ import { PlanNotFoundError, PaymentManagerError } from 'payment-manager';
344
+
345
+ try {
346
+ await pm.subscriptions.create({ userId, planId, currency, productName });
347
+ } catch (error) {
348
+ if (error instanceof PaymentManagerError) {
349
+ console.error(error.code, error.message);
350
+ }
351
+ }
352
+ ```
353
+
354
+ | Error Class | Code | When |
355
+ |---|---|---|
356
+ | `PlanNotFoundError` | `PLAN_NOT_FOUND` | Plan ID does not exist |
357
+ | `PlanInactiveError` | `PLAN_INACTIVE` | Plan exists but is deactivated |
358
+ | `SubscriptionNotFoundError` | `SUBSCRIPTION_NOT_FOUND` | No active subscription for user |
359
+ | `SubscriptionAlreadyActiveError` | `SUBSCRIPTION_ALREADY_ACTIVE` | User already has an active subscription |
360
+ | `TokenPackNotFoundError` | `TOKEN_PACK_NOT_FOUND` | Token pack ID does not exist |
361
+ | `TokenPackInactiveError` | `TOKEN_PACK_INACTIVE` | Token pack exists but is deactivated |
362
+ | `InvalidWebhookSecretError` | `INVALID_WEBHOOK_SECRET` | Webhook `x-secret-key` header mismatch |
363
+ | `PaymentProviderError` | `PAYMENT_PROVIDER_ERROR` | WayForPay API call failed |
364
+ | `InvalidOrderReferenceError` | `INVALID_ORDER_REFERENCE` | Malformed order reference string |
365
+
366
+ ## Webhook Secret Validation
367
+
368
+ Both webhook handlers validate the `x-secret-key` request header against:
369
+ 1. The `webhookSecret` from config (if provided)
370
+ 2. `process.env.NOTIFY_TELEGRAM_USER_SECRET` (fallback)
371
+
372
+ If neither is set, secret validation is skipped.
373
+
374
+ ## Grace Periods
375
+
376
+ The deactivation check handler uses grace periods before expiring subscriptions:
377
+
378
+ | Billing Cycle | Grace Period |
379
+ |---|---|
380
+ | daily | 6 hours |
381
+ | monthly | 1 day |
382
+ | yearly | 1 day |
383
+
384
+ ## TypeScript
385
+
386
+ All types are exported from the package root:
387
+
388
+ ```typescript
389
+ import type {
390
+ PaymentManagerConfig,
391
+ PaymentManager,
392
+ SubscriptionPlanEntity,
393
+ SubscriptionEntity,
394
+ PaymentEntity,
395
+ TokenPackEntity,
396
+ UserTokenBalance,
397
+ WebhookResponse,
398
+ DeactivationResult,
399
+ } from 'payment-manager';
400
+ ```
@@ -0,0 +1,6 @@
1
+ import type { PaymentEntity } from '../types/index.js';
2
+ export declare function create(payment: Omit<PaymentEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<string>;
3
+ export declare function update(paymentId: string, data: Partial<Omit<PaymentEntity, 'id'>>): Promise<void>;
4
+ export declare function findPendingByUserAndPlan(userId: string, planId: string): Promise<PaymentEntity | null>;
5
+ export declare function findPendingByUserAndTokenPack(userId: string, packId: string): Promise<PaymentEntity | null>;
6
+ export declare function findByOrderReference(orderReference: string): Promise<PaymentEntity | null>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.create = create;
4
+ exports.update = update;
5
+ exports.findPendingByUserAndPlan = findPendingByUserAndPlan;
6
+ exports.findPendingByUserAndTokenPack = findPendingByUserAndTokenPack;
7
+ exports.findByOrderReference = findByOrderReference;
8
+ const config_js_1 = require("../config.js");
9
+ const COLLECTION = 'payments';
10
+ function collection() {
11
+ return (0, config_js_1.getConfig)().firestore.collection(COLLECTION);
12
+ }
13
+ async function create(payment) {
14
+ const now = new Date();
15
+ const docRef = await collection().add({
16
+ ...payment,
17
+ createdAt: now,
18
+ updatedAt: now,
19
+ });
20
+ return docRef.id;
21
+ }
22
+ async function update(paymentId, data) {
23
+ await collection()
24
+ .doc(paymentId)
25
+ .update({
26
+ ...data,
27
+ updatedAt: new Date(),
28
+ });
29
+ }
30
+ async function findPendingByUserAndPlan(userId, planId) {
31
+ const snapshot = await collection()
32
+ .where('userId', '==', userId)
33
+ .where('planId', '==', planId)
34
+ .where('status', '==', 'pending')
35
+ .limit(1)
36
+ .get();
37
+ if (snapshot.empty)
38
+ return null;
39
+ const doc = snapshot.docs[0];
40
+ return { id: doc.id, ...doc.data() };
41
+ }
42
+ async function findPendingByUserAndTokenPack(userId, packId) {
43
+ const snapshot = await collection()
44
+ .where('userId', '==', userId)
45
+ .where('tokenPackId', '==', packId)
46
+ .where('status', '==', 'pending')
47
+ .limit(1)
48
+ .get();
49
+ if (snapshot.empty)
50
+ return null;
51
+ const doc = snapshot.docs[0];
52
+ return { id: doc.id, ...doc.data() };
53
+ }
54
+ async function findByOrderReference(orderReference) {
55
+ const snapshot = await collection().where('orderReference', '==', orderReference).limit(1).get();
56
+ if (snapshot.empty)
57
+ return null;
58
+ const doc = snapshot.docs[0];
59
+ return { id: doc.id, ...doc.data() };
60
+ }
@@ -0,0 +1,6 @@
1
+ import type { SubscriptionPlanEntity } from '../types/index.js';
2
+ export declare function getById(planId: string): Promise<SubscriptionPlanEntity | null>;
3
+ export declare function getAll(): Promise<SubscriptionPlanEntity[]>;
4
+ export declare function create(plan: Omit<SubscriptionPlanEntity, 'createdAt' | 'updatedAt'>): Promise<void>;
5
+ export declare function update(planId: string, data: Partial<Omit<SubscriptionPlanEntity, 'id'>>): Promise<void>;
6
+ export declare function upsert(plan: Omit<SubscriptionPlanEntity, 'createdAt' | 'updatedAt'>): Promise<void>;