@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
@@ -0,0 +1,183 @@
1
+ import { getConfig } from '../config.js';
2
+ import * as subscriptionsCollection from '../collections/subscriptions.js';
3
+ import * as plansCollection from '../collections/plans.js';
4
+ import * as paymentService from './paymentService.js';
5
+ import { calculateUpgradeProration, cycleDays } from '../utils/proration.js';
6
+ import { cancelRegularPurchase, editRegularPurchase } from '@misterhomer1992/wayforpay-api';
7
+ import {
8
+ PlanNotFoundError,
9
+ PlanInactiveError,
10
+ SubscriptionNotFoundError,
11
+ PaymentProviderError,
12
+ PaymentManagerError,
13
+ } from '../types/errors.js';
14
+ import type { SubscriptionEntity } from '../types/index.js';
15
+
16
+ export async function create(params: {
17
+ userId: string;
18
+ planId: string;
19
+ currency: string;
20
+ productName: string;
21
+ }): Promise<{ paymentUrl: string }> {
22
+ const { logger } = getConfig();
23
+ const { userId, planId, currency, productName } = params;
24
+
25
+ const existing = await subscriptionsCollection.getActiveByUserId(userId);
26
+ if (existing) {
27
+ await subscriptionsCollection.update(existing.id, {
28
+ status: 'cancelled',
29
+ });
30
+ }
31
+
32
+ const plan = await plansCollection.getById(planId);
33
+ if (!plan) throw new PlanNotFoundError(planId);
34
+ if (!plan.isActive) throw new PlanInactiveError(planId);
35
+
36
+ const result = await paymentService.createSubscriptionPayment(userId, planId, currency, productName);
37
+
38
+ logger?.info('Subscription created', { userId, planId });
39
+ return result;
40
+ }
41
+
42
+ export async function expire(userId: string): Promise<void> {
43
+ const { logger } = getConfig();
44
+
45
+ const subscription = await subscriptionsCollection.getActiveByUserId(userId);
46
+ if (!subscription) throw new SubscriptionNotFoundError(userId);
47
+
48
+ await subscriptionsCollection.update(subscription.id, {
49
+ status: 'expired',
50
+ });
51
+
52
+ logger?.info('Subscription expired', {
53
+ userId,
54
+ subscriptionId: subscription.id,
55
+ });
56
+ }
57
+
58
+ export async function deactivate(userId: string): Promise<void> {
59
+ const { wayforpay, logger } = getConfig();
60
+
61
+ const subscription = await subscriptionsCollection.getActiveByUserId(userId);
62
+ if (!subscription) throw new SubscriptionNotFoundError(userId);
63
+
64
+ try {
65
+ await cancelRegularPurchase({
66
+ merchantAccount: wayforpay.merchantAccount,
67
+ orderReference: subscription.id,
68
+ merchantPassword: wayforpay.merchantSecretKey,
69
+ });
70
+ } catch (error) {
71
+ throw new PaymentProviderError(
72
+ `Failed to cancel regular purchase: ${error instanceof Error ? error.message : String(error)}`,
73
+ );
74
+ }
75
+
76
+ await subscriptionsCollection.update(subscription.id, {
77
+ status: 'cancelled',
78
+ });
79
+
80
+ logger?.info('Subscription cancelled', {
81
+ userId,
82
+ subscriptionId: subscription.id,
83
+ });
84
+ }
85
+
86
+ export async function changePlan(userId: string, newPlanId: string): Promise<void> {
87
+ const { wayforpay, logger } = getConfig();
88
+
89
+ const subscription = await subscriptionsCollection.getActiveByUserId(userId);
90
+ if (!subscription) throw new SubscriptionNotFoundError(userId);
91
+
92
+ const [currentPlan, newPlan] = await Promise.all([
93
+ plansCollection.getById(subscription.planId),
94
+ plansCollection.getById(newPlanId),
95
+ ]);
96
+
97
+ if (!currentPlan) throw new PlanNotFoundError(subscription.planId);
98
+ if (!newPlan) throw new PlanNotFoundError(newPlanId);
99
+ if (!newPlan.isActive) throw new PlanInactiveError(newPlanId);
100
+
101
+ if (newPlan.amount === currentPlan.amount) {
102
+ throw new PaymentManagerError('Cannot change to a plan with the same price', 'SAME_PRICE_PLAN_CHANGE');
103
+ }
104
+
105
+ const now = new Date();
106
+ const isUpgrade = newPlan.amount > currentPlan.amount;
107
+
108
+ if (isUpgrade) {
109
+ const proration = calculateUpgradeProration(
110
+ currentPlan,
111
+ newPlan,
112
+ subscription.startedAt,
113
+ subscription.expiresAt,
114
+ now,
115
+ );
116
+
117
+ await subscriptionsCollection.update(subscription.id, {
118
+ planId: newPlanId,
119
+ expiresAt: proration.newExpiresAt,
120
+ });
121
+
122
+ try {
123
+ await editRegularPurchase({
124
+ merchantAccount: wayforpay.merchantAccount,
125
+ merchantPassword: wayforpay.merchantSecretKey,
126
+ orderReference: subscription.id,
127
+ amount: newPlan.amount,
128
+ currency: 'UAH',
129
+ regularMode: newPlan.regularMode,
130
+ dateBegin: now.toISOString().split('T')[0],
131
+ dateEnd: '',
132
+ });
133
+ } catch (error) {
134
+ throw new PaymentProviderError(
135
+ `Failed to edit regular purchase: ${error instanceof Error ? error.message : String(error)}`,
136
+ );
137
+ }
138
+
139
+ logger?.info('Subscription upgraded', {
140
+ userId,
141
+ subscriptionId: subscription.id,
142
+ oldPlanId: subscription.planId,
143
+ newPlanId,
144
+ newExpiresAt: proration.newExpiresAt.toISOString(),
145
+ });
146
+ } else {
147
+ const pendingPlanChangeDate = subscription.expiresAt;
148
+
149
+ await subscriptionsCollection.update(subscription.id, {
150
+ pendingPlanId: newPlanId,
151
+ pendingPlanChangeDate,
152
+ });
153
+
154
+ try {
155
+ await editRegularPurchase({
156
+ merchantAccount: wayforpay.merchantAccount,
157
+ merchantPassword: wayforpay.merchantSecretKey,
158
+ orderReference: subscription.id,
159
+ amount: newPlan.amount,
160
+ currency: 'UAH',
161
+ regularMode: newPlan.regularMode,
162
+ dateBegin: pendingPlanChangeDate.toISOString().split('T')[0],
163
+ dateEnd: '',
164
+ });
165
+ } catch (error) {
166
+ throw new PaymentProviderError(
167
+ `Failed to edit regular purchase: ${error instanceof Error ? error.message : String(error)}`,
168
+ );
169
+ }
170
+
171
+ logger?.info('Subscription downgraded', {
172
+ userId,
173
+ subscriptionId: subscription.id,
174
+ oldPlanId: subscription.planId,
175
+ newPlanId,
176
+ pendingPlanChangeDate: pendingPlanChangeDate.toISOString(),
177
+ });
178
+ }
179
+ }
180
+
181
+ export async function getActiveByUserId(userId: string): Promise<SubscriptionEntity | null> {
182
+ return subscriptionsCollection.getActiveByUserId(userId);
183
+ }
@@ -0,0 +1,54 @@
1
+ import { getConfig } from '../config.js';
2
+ import * as tokenPacksCollection from '../collections/tokenPacks.js';
3
+ import * as userTokenBalancesCollection from '../collections/userTokenBalances.js';
4
+ import * as paymentService from './paymentService.js';
5
+ import { TokenPackNotFoundError, TokenPackInactiveError } from '../types/errors.js';
6
+ import type { TokenPackEntity, UserTokenBalance } from '../types/index.js';
7
+
8
+ export async function create(pack: Omit<TokenPackEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> {
9
+ const { logger } = getConfig();
10
+
11
+ const packId = await tokenPacksCollection.create(pack);
12
+
13
+ logger?.info('Token pack created', { packId, name: pack.name, tokenAmount: pack.tokenAmount, price: pack.price });
14
+ return packId;
15
+ }
16
+
17
+ export async function remove(packId: string): Promise<void> {
18
+ await tokenPacksCollection.remove(packId);
19
+ }
20
+
21
+ export async function activate(packId: string): Promise<void> {
22
+ await tokenPacksCollection.update(packId, { isActive: true });
23
+ }
24
+
25
+ export async function deactivate(packId: string): Promise<void> {
26
+ await tokenPacksCollection.update(packId, { isActive: false });
27
+ }
28
+
29
+ export async function getAll(): Promise<TokenPackEntity[]> {
30
+ return tokenPacksCollection.getAll();
31
+ }
32
+
33
+ export async function buyExtraTokens(params: {
34
+ userId: string;
35
+ packId: string;
36
+ currency: string;
37
+ productName: string;
38
+ }): Promise<{ paymentUrl: string }> {
39
+ const { userId, packId, currency, productName } = params;
40
+ const { logger } = getConfig();
41
+
42
+ const pack = await tokenPacksCollection.getById(packId);
43
+ if (!pack) throw new TokenPackNotFoundError(packId);
44
+ if (!pack.isActive) throw new TokenPackInactiveError(packId);
45
+
46
+ const result = await paymentService.createTokenPackPayment(userId, packId, currency, productName, pack.price);
47
+
48
+ logger?.info('Token pack purchase initiated', { userId, packId, price: pack.price });
49
+ return result;
50
+ }
51
+
52
+ export async function getBalance(userId: string): Promise<UserTokenBalance> {
53
+ return userTokenBalancesCollection.getByUserId(userId);
54
+ }
@@ -0,0 +1,217 @@
1
+ import { getConfig } from '../config.js';
2
+ import { parseOrderReference } from '../utils/orderReference.js';
3
+ import { buildWebhookResponse } from '../utils/webhookResponse.js';
4
+ import { cycleDays } from '../utils/proration.js';
5
+ import { getExpirationWithGrace } from '../utils/gracePeriod.js';
6
+ import * as paymentsCollection from '../collections/payments.js';
7
+ import * as subscriptionsCollection from '../collections/subscriptions.js';
8
+ import * as plansCollection from '../collections/plans.js';
9
+ import * as tokenPacksCollection from '../collections/tokenPacks.js';
10
+ import * as userTokenBalancesCollection from '../collections/userTokenBalances.js';
11
+ import { InvalidWebhookSecretError } from '../types/errors.js';
12
+ import type { WebhookResponse, DeactivationResult, ServiceUrlRequestParams } from '../types/webhook.js';
13
+
14
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
15
+ const SUCCESS_REASON_CODES = [1100, 4100];
16
+
17
+ export async function handlePaymentCheck(
18
+ reqBody: Record<string, unknown>,
19
+ headers: Record<string, string | undefined>,
20
+ ): Promise<WebhookResponse> {
21
+ const { wayforpay, webhookSecret, logger } = getConfig();
22
+
23
+ // Validate webhook secret
24
+ const secret = webhookSecret ?? process.env.NOTIFY_TELEGRAM_USER_SECRET;
25
+ const headerSecret = headers['x-secret-key'];
26
+
27
+ if (secret && headerSecret !== secret) {
28
+ throw new InvalidWebhookSecretError();
29
+ }
30
+
31
+ // Parse request body (WayForPay sends body as JSON key)
32
+ const parsed: ServiceUrlRequestParams = JSON.parse(Object.keys(reqBody)[0]);
33
+
34
+ const { orderReference, reasonCode } = parsed;
35
+
36
+ logger?.info('Webhook received', { orderReference, reasonCode });
37
+
38
+ // Check if payment is successful
39
+ if (!SUCCESS_REASON_CODES.includes(reasonCode)) {
40
+ return buildWebhookResponse(orderReference, 'reject', wayforpay.merchantSecretKey);
41
+ }
42
+
43
+ const ref = parseOrderReference(orderReference);
44
+
45
+ if (ref.type === 'sub') {
46
+ await handleSubscriptionPayment(ref.userId, ref.planId!, orderReference);
47
+ } else {
48
+ await handleTokenPackPayment(ref.userId, ref.tokenPackId!, orderReference);
49
+ }
50
+
51
+ // Update payment status to approved
52
+ const payment = await paymentsCollection.findByOrderReference(orderReference);
53
+ if (payment) {
54
+ await paymentsCollection.update(payment.id, { status: 'approved' });
55
+ }
56
+
57
+ return buildWebhookResponse(orderReference, 'accept', wayforpay.merchantSecretKey);
58
+ }
59
+
60
+ async function handleSubscriptionPayment(userId: string, planId: string, orderReference: string): Promise<void> {
61
+ const { logger } = getConfig();
62
+
63
+ const existing = await subscriptionsCollection.getActiveByUserId(userId);
64
+
65
+ if (existing) {
66
+ // Recurrent payment — check for pending downgrade first
67
+ const now = new Date();
68
+ if (existing.pendingPlanId && existing.pendingPlanChangeDate && existing.pendingPlanChangeDate <= now) {
69
+ // Apply pending downgrade
70
+ const newPlanId = existing.pendingPlanId;
71
+ const plan = await plansCollection.getById(newPlanId);
72
+ const days = plan ? cycleDays(plan.regularMode) : cycleDays('monthly');
73
+ const newExpiresAt = new Date(now.getTime() + days * MS_PER_DAY);
74
+
75
+ await subscriptionsCollection.update(existing.id, {
76
+ planId: newPlanId,
77
+ expiresAt: newExpiresAt,
78
+ startedAt: now,
79
+ pendingPlanId: undefined,
80
+ pendingPlanChangeDate: undefined,
81
+ });
82
+
83
+ logger?.info('Subscription downgrade applied', {
84
+ userId,
85
+ subscriptionId: existing.id,
86
+ oldPlanId: existing.planId,
87
+ newPlanId,
88
+ });
89
+ } else {
90
+ // Extend subscription
91
+ const plan = await plansCollection.getById(existing.planId);
92
+ const days = plan ? cycleDays(plan.regularMode) : cycleDays('monthly');
93
+ const newExpiresAt = new Date(existing.expiresAt.getTime() + days * MS_PER_DAY);
94
+
95
+ await subscriptionsCollection.update(existing.id, {
96
+ expiresAt: newExpiresAt,
97
+ });
98
+
99
+ logger?.info('Subscription renewed', {
100
+ userId,
101
+ subscriptionId: existing.id,
102
+ newExpiresAt: newExpiresAt.toISOString(),
103
+ });
104
+ }
105
+ } else {
106
+ // First payment — create new subscription
107
+ const plan = await plansCollection.getById(planId);
108
+ const days = plan ? cycleDays(plan.regularMode) : cycleDays('monthly');
109
+ const now = new Date();
110
+ const expiresAt = new Date(now.getTime() + days * MS_PER_DAY);
111
+
112
+ const subscriptionId = await subscriptionsCollection.create({
113
+ userId,
114
+ planId,
115
+ status: 'active',
116
+ startedAt: now,
117
+ expiresAt,
118
+ });
119
+
120
+ logger?.info('Subscription created', {
121
+ userId,
122
+ subscriptionId,
123
+ planId,
124
+ expiresAt: expiresAt.toISOString(),
125
+ });
126
+ }
127
+ }
128
+
129
+ async function handleTokenPackPayment(userId: string, tokenPackId: string, orderReference: string): Promise<void> {
130
+ const { logger } = getConfig();
131
+
132
+ const pack = await tokenPacksCollection.getById(tokenPackId);
133
+ if (!pack) {
134
+ logger?.warn('Token pack not found for webhook', { tokenPackId, orderReference });
135
+ return;
136
+ }
137
+
138
+ await userTokenBalancesCollection.addTokens(userId, pack.tokenAmount);
139
+
140
+ logger?.info('Tokens added', {
141
+ userId,
142
+ tokenPackId,
143
+ tokenAmount: pack.tokenAmount,
144
+ });
145
+ }
146
+
147
+ export async function handleSubscriptionDeactivationCheck(
148
+ headers: Record<string, string | undefined>,
149
+ ): Promise<DeactivationResult> {
150
+ const { webhookSecret, logger } = getConfig();
151
+
152
+ const secret = webhookSecret ?? process.env.NOTIFY_TELEGRAM_USER_SECRET;
153
+ const headerSecret = headers['x-secret-key'];
154
+
155
+ if (secret && headerSecret !== secret) {
156
+ throw new InvalidWebhookSecretError();
157
+ }
158
+
159
+ const subscriptions = await subscriptionsCollection.getAllActiveAndCancelled();
160
+ const now = new Date();
161
+
162
+ const expired: string[] = [];
163
+ const downgraded: string[] = [];
164
+
165
+ for (const subscription of subscriptions) {
166
+ const plan = await plansCollection.getById(subscription.planId);
167
+ if (!plan) continue;
168
+
169
+ const expirationWithGrace = getExpirationWithGrace(subscription.expiresAt, plan.regularMode);
170
+
171
+ if (now > expirationWithGrace) {
172
+ await subscriptionsCollection.update(subscription.id, { status: 'expired' });
173
+ expired.push(subscription.userId);
174
+
175
+ logger?.info('Subscription expired by deactivation check', {
176
+ userId: subscription.userId,
177
+ subscriptionId: subscription.id,
178
+ });
179
+ continue;
180
+ }
181
+
182
+ if (
183
+ subscription.pendingPlanId &&
184
+ subscription.pendingPlanChangeDate &&
185
+ subscription.pendingPlanChangeDate <= now
186
+ ) {
187
+ const newPlanId = subscription.pendingPlanId;
188
+ const newPlan = await plansCollection.getById(newPlanId);
189
+ const days = newPlan ? cycleDays(newPlan.regularMode) : cycleDays('monthly');
190
+ const newExpiresAt = new Date(now.getTime() + days * MS_PER_DAY);
191
+
192
+ await subscriptionsCollection.update(subscription.id, {
193
+ planId: newPlanId,
194
+ expiresAt: newExpiresAt,
195
+ startedAt: now,
196
+ pendingPlanId: undefined,
197
+ pendingPlanChangeDate: undefined,
198
+ });
199
+
200
+ downgraded.push(subscription.userId);
201
+
202
+ logger?.info('Subscription downgrade applied by deactivation check', {
203
+ userId: subscription.userId,
204
+ subscriptionId: subscription.id,
205
+ oldPlanId: subscription.planId,
206
+ newPlanId,
207
+ });
208
+ }
209
+ }
210
+
211
+ logger?.info('Deactivation check completed', {
212
+ expiredCount: expired.length,
213
+ downgradedCount: downgraded.length,
214
+ });
215
+
216
+ return { expired, downgraded };
217
+ }
@@ -0,0 +1,72 @@
1
+ export class PaymentManagerError extends Error {
2
+ public readonly code: string;
3
+
4
+ constructor(message: string, code: string) {
5
+ super(message);
6
+ this.name = 'PaymentManagerError';
7
+ this.code = code;
8
+ }
9
+ }
10
+
11
+ export class PlanNotFoundError extends PaymentManagerError {
12
+ constructor(planId: string) {
13
+ super(`Plan not found: ${planId}`, 'PLAN_NOT_FOUND');
14
+ this.name = 'PlanNotFoundError';
15
+ }
16
+ }
17
+
18
+ export class PlanInactiveError extends PaymentManagerError {
19
+ constructor(planId: string) {
20
+ super(`Plan is inactive: ${planId}`, 'PLAN_INACTIVE');
21
+ this.name = 'PlanInactiveError';
22
+ }
23
+ }
24
+
25
+ export class SubscriptionAlreadyActiveError extends PaymentManagerError {
26
+ constructor(userId: string) {
27
+ super(`User already has an active subscription: ${userId}`, 'SUBSCRIPTION_ALREADY_ACTIVE');
28
+ this.name = 'SubscriptionAlreadyActiveError';
29
+ }
30
+ }
31
+
32
+ export class SubscriptionNotFoundError extends PaymentManagerError {
33
+ constructor(userId: string) {
34
+ super(`No active subscription found for user: ${userId}`, 'SUBSCRIPTION_NOT_FOUND');
35
+ this.name = 'SubscriptionNotFoundError';
36
+ }
37
+ }
38
+
39
+ export class TokenPackNotFoundError extends PaymentManagerError {
40
+ constructor(packId: string) {
41
+ super(`Token pack not found: ${packId}`, 'TOKEN_PACK_NOT_FOUND');
42
+ this.name = 'TokenPackNotFoundError';
43
+ }
44
+ }
45
+
46
+ export class TokenPackInactiveError extends PaymentManagerError {
47
+ constructor(packId: string) {
48
+ super(`Token pack is inactive: ${packId}`, 'TOKEN_PACK_INACTIVE');
49
+ this.name = 'TokenPackInactiveError';
50
+ }
51
+ }
52
+
53
+ export class InvalidWebhookSecretError extends PaymentManagerError {
54
+ constructor() {
55
+ super('Invalid webhook secret', 'INVALID_WEBHOOK_SECRET');
56
+ this.name = 'InvalidWebhookSecretError';
57
+ }
58
+ }
59
+
60
+ export class PaymentProviderError extends PaymentManagerError {
61
+ constructor(message: string) {
62
+ super(message, 'PAYMENT_PROVIDER_ERROR');
63
+ this.name = 'PaymentProviderError';
64
+ }
65
+ }
66
+
67
+ export class InvalidOrderReferenceError extends PaymentManagerError {
68
+ constructor(orderReference: string) {
69
+ super(`Invalid order reference: ${orderReference}`, 'INVALID_ORDER_REFERENCE');
70
+ this.name = 'InvalidOrderReferenceError';
71
+ }
72
+ }
@@ -0,0 +1,18 @@
1
+ export { SubscriptionPlanEntity } from './plan.js';
2
+ export { SubscriptionEntity } from './subscription.js';
3
+ export { PaymentEntity } from './payment.js';
4
+ export { TokenPackEntity, UserTokenBalance } from './tokenPack.js';
5
+ export { WebhookResponse, DeactivationResult, ServiceUrlRequestParams } from './webhook.js';
6
+ export { OrderReferenceParams, ParsedOrderReference } from './orderReference.js';
7
+ export {
8
+ PaymentManagerError,
9
+ PlanNotFoundError,
10
+ PlanInactiveError,
11
+ SubscriptionAlreadyActiveError,
12
+ SubscriptionNotFoundError,
13
+ TokenPackNotFoundError,
14
+ TokenPackInactiveError,
15
+ InvalidWebhookSecretError,
16
+ PaymentProviderError,
17
+ InvalidOrderReferenceError,
18
+ } from './errors.js';
@@ -0,0 +1,19 @@
1
+ export interface OrderReferenceParams {
2
+ version: string;
3
+ platform: string;
4
+ userId: string;
5
+ type: 'sub' | 'tkn';
6
+ planId?: string;
7
+ tokenPackId?: string;
8
+ date: Date;
9
+ }
10
+
11
+ export interface ParsedOrderReference {
12
+ version: string;
13
+ platform: string;
14
+ userId: string;
15
+ type: 'sub' | 'tkn';
16
+ planId?: string;
17
+ tokenPackId?: string;
18
+ date: Date;
19
+ }
@@ -0,0 +1,14 @@
1
+ export interface PaymentEntity {
2
+ id: string;
3
+ userId: string;
4
+ planId?: string;
5
+ tokenPackId?: string;
6
+ amount: number;
7
+ currency: string;
8
+ status: 'pending' | 'approved' | 'rejected';
9
+ orderReference: string;
10
+ paymentLink?: string;
11
+ paymentLinkCreatedAt?: Date;
12
+ createdAt: Date;
13
+ updatedAt: Date;
14
+ }
@@ -0,0 +1,10 @@
1
+ export interface SubscriptionPlanEntity {
2
+ id: string;
3
+ name: string;
4
+ amount: number;
5
+ regularMode: 'daily' | 'monthly' | 'yearly';
6
+ description?: string;
7
+ isActive: boolean;
8
+ createdAt: Date;
9
+ updatedAt: Date;
10
+ }
@@ -0,0 +1,12 @@
1
+ export interface SubscriptionEntity {
2
+ id: string;
3
+ userId: string;
4
+ planId: string;
5
+ status: 'active' | 'cancelled' | 'expired';
6
+ startedAt: Date;
7
+ expiresAt: Date;
8
+ pendingPlanId?: string;
9
+ pendingPlanChangeDate?: Date;
10
+ createdAt: Date;
11
+ updatedAt: Date;
12
+ }
@@ -0,0 +1,16 @@
1
+ export interface TokenPackEntity {
2
+ id: string;
3
+ name: string;
4
+ tokenAmount: number;
5
+ price: number;
6
+ isActive: boolean;
7
+ createdAt: Date;
8
+ updatedAt: Date;
9
+ }
10
+
11
+ export interface UserTokenBalance {
12
+ userId: string;
13
+ balance: number;
14
+ totalPurchased: number;
15
+ updatedAt: Date;
16
+ }
@@ -0,0 +1,34 @@
1
+ export interface WebhookResponse {
2
+ orderReference: string;
3
+ status: 'accept' | 'reject';
4
+ time: string;
5
+ signature: string;
6
+ }
7
+
8
+ export interface DeactivationResult {
9
+ expired: string[];
10
+ downgraded: string[];
11
+ }
12
+
13
+ export interface ServiceUrlRequestParams {
14
+ merchantAccount: string;
15
+ orderReference: string;
16
+ merchantSignature: string;
17
+ amount: number;
18
+ currency: string;
19
+ authCode: string;
20
+ email: string;
21
+ phone: string;
22
+ createdDate: number;
23
+ processingDate: number;
24
+ cardPan: string;
25
+ cardType: string;
26
+ issuerBankCountry: string;
27
+ issuerBankName: string;
28
+ recToken: string;
29
+ transactionStatus: string;
30
+ reason: string;
31
+ reasonCode: number;
32
+ fee: number;
33
+ paymentSystem: string;
34
+ }