@misterhomer1992/miit-bot-payment 1.1.6 → 2.0.4
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/dist/config/ConfigurationManager.d.ts +64 -0
- package/dist/config/ConfigurationManager.d.ts.map +1 -0
- package/dist/config/ConfigurationManager.js +144 -0
- package/dist/config/ConfigurationManager.js.map +1 -0
- package/dist/config/defaults.d.ts +18 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +26 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/environment.d.ts +38 -0
- package/dist/config/environment.d.ts.map +1 -0
- package/dist/config/environment.js +91 -0
- package/dist/config/environment.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +18 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +53 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +3 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/cache/InMemoryCache.d.ts +17 -0
- package/dist/modules/cache/InMemoryCache.d.ts.map +1 -0
- package/dist/modules/cache/InMemoryCache.js +77 -0
- package/dist/modules/cache/InMemoryCache.js.map +1 -0
- package/dist/modules/cache/index.d.ts +3 -0
- package/dist/modules/cache/index.d.ts.map +1 -0
- package/dist/modules/cache/index.js +19 -0
- package/dist/modules/cache/index.js.map +1 -0
- package/dist/modules/cache/types.d.ts +52 -0
- package/dist/modules/cache/types.d.ts.map +1 -0
- package/dist/modules/cache/types.js +3 -0
- package/dist/modules/cache/types.js.map +1 -0
- package/dist/modules/errors/index.d.ts +2 -0
- package/dist/modules/errors/index.d.ts.map +1 -0
- package/dist/modules/errors/index.js +19 -0
- package/dist/modules/errors/index.js.map +1 -0
- package/dist/modules/errors/types.d.ts +112 -0
- package/dist/modules/errors/types.d.ts.map +1 -0
- package/dist/modules/errors/types.js +174 -0
- package/dist/modules/errors/types.js.map +1 -0
- package/dist/modules/payments/api.d.ts +63 -1
- package/dist/modules/payments/api.d.ts.map +1 -1
- package/dist/modules/payments/api.js +103 -1
- package/dist/modules/payments/api.js.map +1 -1
- package/dist/modules/payments/const.d.ts.map +1 -1
- package/dist/modules/payments/const.js +1 -0
- package/dist/modules/payments/const.js.map +1 -1
- package/dist/modules/payments/index.d.ts +8 -0
- package/dist/modules/payments/index.d.ts.map +1 -1
- package/dist/modules/payments/index.js +8 -0
- package/dist/modules/payments/index.js.map +1 -1
- package/dist/modules/payments/service.d.ts +42 -2
- package/dist/modules/payments/service.d.ts.map +1 -1
- package/dist/modules/payments/service.js +132 -3
- package/dist/modules/payments/service.js.map +1 -1
- package/dist/modules/payments/subscription-check-webhook.handler.d.ts +85 -0
- package/dist/modules/payments/subscription-check-webhook.handler.d.ts.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.handler.js +155 -0
- package/dist/modules/payments/subscription-check-webhook.handler.js.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.service.d.ts +59 -0
- package/dist/modules/payments/subscription-check-webhook.service.d.ts.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.service.js +330 -0
- package/dist/modules/payments/subscription-check-webhook.service.js.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.types.d.ts +25 -0
- package/dist/modules/payments/subscription-check-webhook.types.d.ts.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.types.js +3 -0
- package/dist/modules/payments/subscription-check-webhook.types.js.map +1 -0
- package/dist/modules/payments/types.d.ts +69 -2
- package/dist/modules/payments/types.d.ts.map +1 -1
- package/dist/modules/payments/utils.d.ts +151 -5
- package/dist/modules/payments/utils.d.ts.map +1 -1
- package/dist/modules/payments/utils.js +253 -9
- package/dist/modules/payments/utils.js.map +1 -1
- package/dist/modules/payments/wayforpay.service.d.ts +39 -0
- package/dist/modules/payments/wayforpay.service.d.ts.map +1 -0
- package/dist/modules/payments/wayforpay.service.js +217 -0
- package/dist/modules/payments/wayforpay.service.js.map +1 -0
- package/dist/modules/payments/wayforpay.types.d.ts +115 -0
- package/dist/modules/payments/wayforpay.types.d.ts.map +1 -0
- package/dist/modules/payments/wayforpay.types.js +3 -0
- package/dist/modules/payments/wayforpay.types.js.map +1 -0
- package/dist/modules/payments/webhook.handler.d.ts +98 -0
- package/dist/modules/payments/webhook.handler.d.ts.map +1 -0
- package/dist/modules/payments/webhook.handler.js +153 -0
- package/dist/modules/payments/webhook.handler.js.map +1 -0
- package/dist/modules/payments/webhook.service.d.ts +99 -0
- package/dist/modules/payments/webhook.service.d.ts.map +1 -0
- package/dist/modules/payments/webhook.service.js +672 -0
- package/dist/modules/payments/webhook.service.js.map +1 -0
- package/dist/modules/payments/webhook.types.d.ts +35 -0
- package/dist/modules/payments/webhook.types.d.ts.map +1 -0
- package/dist/modules/payments/webhook.types.js +3 -0
- package/dist/modules/payments/webhook.types.js.map +1 -0
- package/dist/modules/subscription/change.service.d.ts +80 -0
- package/dist/modules/subscription/change.service.d.ts.map +1 -0
- package/dist/modules/subscription/change.service.js +226 -0
- package/dist/modules/subscription/change.service.js.map +1 -0
- package/dist/modules/subscription/index.d.ts +2 -0
- package/dist/modules/subscription/index.d.ts.map +1 -1
- package/dist/modules/subscription/index.js +2 -0
- package/dist/modules/subscription/index.js.map +1 -1
- package/dist/modules/subscription/service.d.ts +8 -1
- package/dist/modules/subscription/service.d.ts.map +1 -1
- package/dist/modules/subscription/service.js +59 -2
- package/dist/modules/subscription/service.js.map +1 -1
- package/dist/modules/subscription/status-check.handler.d.ts +117 -0
- package/dist/modules/subscription/status-check.handler.d.ts.map +1 -0
- package/dist/modules/subscription/status-check.handler.js +164 -0
- package/dist/modules/subscription/status-check.handler.js.map +1 -0
- package/dist/modules/subscription/types.d.ts +37 -1
- package/dist/modules/subscription/types.d.ts.map +1 -1
- package/dist/modules/subscriptionPlan/const.d.ts +5 -0
- package/dist/modules/subscriptionPlan/const.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/const.js +106 -0
- package/dist/modules/subscriptionPlan/const.js.map +1 -0
- package/dist/modules/subscriptionPlan/index.d.ts +5 -0
- package/dist/modules/subscriptionPlan/index.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/index.js +21 -0
- package/dist/modules/subscriptionPlan/index.js.map +1 -0
- package/dist/modules/subscriptionPlan/repository.d.ts +22 -0
- package/dist/modules/subscriptionPlan/repository.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/repository.js +95 -0
- package/dist/modules/subscriptionPlan/repository.js.map +1 -0
- package/dist/modules/subscriptionPlan/service.d.ts +21 -0
- package/dist/modules/subscriptionPlan/service.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/service.js +128 -0
- package/dist/modules/subscriptionPlan/service.js.map +1 -0
- package/dist/modules/subscriptionPlan/types.d.ts +40 -0
- package/dist/modules/subscriptionPlan/types.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/types.js +3 -0
- package/dist/modules/subscriptionPlan/types.js.map +1 -0
- package/dist/modules/token/const.d.ts +7 -0
- package/dist/modules/token/const.d.ts.map +1 -0
- package/dist/modules/token/const.js +66 -0
- package/dist/modules/token/const.js.map +1 -0
- package/dist/modules/token/index.d.ts +4 -0
- package/dist/modules/token/index.d.ts.map +1 -0
- package/dist/modules/token/index.js +20 -0
- package/dist/modules/token/index.js.map +1 -0
- package/dist/modules/token/service.d.ts +46 -0
- package/dist/modules/token/service.d.ts.map +1 -0
- package/dist/modules/token/service.js +249 -0
- package/dist/modules/token/service.js.map +1 -0
- package/dist/modules/token/types.d.ts +109 -0
- package/dist/modules/token/types.d.ts.map +1 -0
- package/dist/modules/token/types.js +3 -0
- package/dist/modules/token/types.js.map +1 -0
- package/dist/modules/tokenPack/const.d.ts +4 -0
- package/dist/modules/tokenPack/const.d.ts.map +1 -0
- package/dist/modules/tokenPack/const.js +10 -0
- package/dist/modules/tokenPack/const.js.map +1 -0
- package/dist/modules/tokenPack/index.d.ts +5 -0
- package/dist/modules/tokenPack/index.d.ts.map +1 -0
- package/dist/modules/tokenPack/index.js +21 -0
- package/dist/modules/tokenPack/index.js.map +1 -0
- package/dist/modules/tokenPack/repository.d.ts +32 -0
- package/dist/modules/tokenPack/repository.d.ts.map +1 -0
- package/dist/modules/tokenPack/repository.js +103 -0
- package/dist/modules/tokenPack/repository.js.map +1 -0
- package/dist/modules/tokenPack/service.d.ts +28 -0
- package/dist/modules/tokenPack/service.d.ts.map +1 -0
- package/dist/modules/tokenPack/service.js +106 -0
- package/dist/modules/tokenPack/service.js.map +1 -0
- package/dist/modules/tokenPack/types.d.ts +124 -0
- package/dist/modules/tokenPack/types.d.ts.map +1 -0
- package/dist/modules/tokenPack/types.js +3 -0
- package/dist/modules/tokenPack/types.js.map +1 -0
- package/package.json +9 -5
- package/src/config/ConfigurationManager.ts +159 -0
- package/src/config/defaults.ts +27 -0
- package/src/config/environment.ts +94 -0
- package/src/config/index.ts +22 -0
- package/src/config/types.ts +56 -0
- package/src/index.ts +29 -0
- package/src/modules/cache/InMemoryCache.ts +98 -0
- package/src/modules/cache/index.ts +2 -0
- package/src/modules/cache/types.ts +60 -0
- package/src/modules/cancellableAPI/utils.ts +60 -0
- package/src/modules/errors/index.ts +16 -0
- package/src/modules/errors/types.ts +201 -0
- package/src/modules/invoice/const.ts +7 -0
- package/src/modules/invoice/index.ts +4 -0
- package/src/modules/invoice/repository.ts +52 -0
- package/src/modules/invoice/service.ts +44 -0
- package/src/modules/invoice/types.ts +47 -0
- package/src/modules/logger/types.ts +8 -0
- package/src/modules/network/utils.ts +24 -0
- package/src/modules/payments/api.ts +289 -0
- package/src/modules/payments/const.ts +11 -0
- package/src/modules/payments/index.ts +14 -0
- package/src/modules/payments/repository.ts +125 -0
- package/src/modules/payments/service.test.ts +400 -0
- package/src/modules/payments/service.ts +365 -0
- package/src/modules/payments/subscription-check-webhook.handler.integration.test.ts +935 -0
- package/src/modules/payments/subscription-check-webhook.handler.ts +211 -0
- package/src/modules/payments/subscription-check-webhook.service.ts +398 -0
- package/src/modules/payments/subscription-check-webhook.types.ts +29 -0
- package/src/modules/payments/types.ts +193 -0
- package/src/modules/payments/utils.ts +428 -0
- package/src/modules/payments/wayforpay.service.test.ts +375 -0
- package/src/modules/payments/wayforpay.service.ts +284 -0
- package/src/modules/payments/wayforpay.types.ts +138 -0
- package/src/modules/payments/webhook.handler.integration.test.ts +975 -0
- package/src/modules/payments/webhook.handler.ts +219 -0
- package/src/modules/payments/webhook.service.ts +812 -0
- package/src/modules/payments/webhook.types.ts +38 -0
- package/src/modules/subscription/change.service.ts +317 -0
- package/src/modules/subscription/const.ts +9 -0
- package/src/modules/subscription/index.ts +5 -0
- package/src/modules/subscription/repository.ts +277 -0
- package/src/modules/subscription/service.test.ts +665 -0
- package/src/modules/subscription/service.ts +328 -0
- package/src/modules/subscription/status-check.handler.ts +254 -0
- package/src/modules/subscription/types.ts +267 -0
- package/src/modules/subscription/utils.ts +5 -0
- package/src/modules/subscriptionPlan/const.ts +106 -0
- package/src/modules/subscriptionPlan/index.ts +4 -0
- package/src/modules/subscriptionPlan/repository.ts +129 -0
- package/src/modules/subscriptionPlan/service.test.ts +401 -0
- package/src/modules/subscriptionPlan/service.ts +148 -0
- package/src/modules/subscriptionPlan/types.ts +67 -0
- package/src/modules/token/const.ts +64 -0
- package/src/modules/token/index.ts +3 -0
- package/src/modules/token/service.test.ts +499 -0
- package/src/modules/token/service.ts +297 -0
- package/src/modules/token/types.ts +124 -0
- package/src/modules/tokenPack/const.ts +9 -0
- package/src/modules/tokenPack/index.ts +4 -0
- package/src/modules/tokenPack/repository.ts +144 -0
- package/src/modules/tokenPack/service.ts +119 -0
- package/src/modules/tokenPack/types.ts +131 -0
- package/src/modules/user/index.ts +3 -0
- package/src/modules/user/types.ts +143 -0
- package/src/modules/user/userRepository.ts +64 -0
- package/src/modules/user/userService.ts +68 -0
- package/src/types/extend-express.d.ts +16 -0
- package/src/types/function.ts +5 -0
- package/src/types/utilities.ts +22 -0
- package/src/utils.ts +53 -0
- package/tsconfig.json +29 -0
- package/dist/modules/subscription/subscriptionPlan.d.ts +0 -4
- package/dist/modules/subscription/subscriptionPlan.d.ts.map +0 -1
- package/dist/modules/subscription/subscriptionPlan.js +0 -67
- package/dist/modules/subscription/subscriptionPlan.js.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { WayForPayCallbackData, WayForPayWebhookResponse } from './wayforpay.types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of processing a webhook callback
|
|
5
|
+
*/
|
|
6
|
+
type WebhookProcessingResult = {
|
|
7
|
+
success: boolean;
|
|
8
|
+
response: WayForPayWebhookResponse;
|
|
9
|
+
error?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parameters for activating a subscription from webhook
|
|
14
|
+
*/
|
|
15
|
+
type ActivateSubscriptionParams = {
|
|
16
|
+
userId: string;
|
|
17
|
+
platform: string;
|
|
18
|
+
planId: string;
|
|
19
|
+
orderReference: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interface for webhook handler service operations.
|
|
24
|
+
* Handles payment confirmation webhooks from WayForPay.
|
|
25
|
+
*/
|
|
26
|
+
interface IWebhookHandlerService {
|
|
27
|
+
/**
|
|
28
|
+
* Processes a payment confirmation webhook from WayForPay.
|
|
29
|
+
* Verifies signature, updates payment status, activates subscription,
|
|
30
|
+
* and initializes user token balance.
|
|
31
|
+
*
|
|
32
|
+
* @param callbackData - The callback data from WayForPay
|
|
33
|
+
* @returns Processing result with response to send back to WayForPay
|
|
34
|
+
*/
|
|
35
|
+
processPaymentWebhook(callbackData: WayForPayCallbackData): Promise<WebhookProcessingResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type { WebhookProcessingResult, ActivateSubscriptionParams, IWebhookHandlerService };
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { Logger } from '../logger/types';
|
|
3
|
+
import { SubscriptionService } from './service';
|
|
4
|
+
import { SubscriptionPlanService } from '../subscriptionPlan/service';
|
|
5
|
+
import { PaymentService } from '../payments/service';
|
|
6
|
+
import type { ISubscriptionService, SubscriptionEntity, UpgradePreview, UpgradeResult } from './types';
|
|
7
|
+
import type { ISubscriptionPlanService, SubscriptionPlanEntity } from '../subscriptionPlan/types';
|
|
8
|
+
import type { IPaymentService } from '../payments/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Error codes for subscription change operations
|
|
12
|
+
*/
|
|
13
|
+
export const SubscriptionChangeErrorCodes = {
|
|
14
|
+
NO_ACTIVE_SUBSCRIPTION: 'NO_ACTIVE_SUBSCRIPTION',
|
|
15
|
+
SUBSCRIPTION_EXPIRED: 'SUBSCRIPTION_EXPIRED',
|
|
16
|
+
SAME_PLAN: 'SAME_PLAN',
|
|
17
|
+
DOWNGRADE_NOT_ALLOWED: 'DOWNGRADE_NOT_ALLOWED',
|
|
18
|
+
UPGRADE_NOT_ALLOWED: 'UPGRADE_NOT_ALLOWED',
|
|
19
|
+
PLAN_NOT_FOUND: 'PLAN_NOT_FOUND',
|
|
20
|
+
CURRENCY_MISMATCH: 'CURRENCY_MISMATCH',
|
|
21
|
+
INVALID_UPGRADE_COST: 'INVALID_UPGRADE_COST',
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type SubscriptionChangeErrorCode = (typeof SubscriptionChangeErrorCodes)[keyof typeof SubscriptionChangeErrorCodes];
|
|
25
|
+
|
|
26
|
+
export class SubscriptionChangeError extends Error {
|
|
27
|
+
public readonly code: SubscriptionChangeErrorCode;
|
|
28
|
+
|
|
29
|
+
constructor(code: SubscriptionChangeErrorCode, message: string) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.name = 'SubscriptionChangeError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Service for handling subscription upgrades and downgrades.
|
|
38
|
+
*/
|
|
39
|
+
export class SubscriptionChangeService {
|
|
40
|
+
private readonly logger: Logger;
|
|
41
|
+
private readonly subscriptionService: ISubscriptionService;
|
|
42
|
+
private readonly subscriptionPlanService: ISubscriptionPlanService;
|
|
43
|
+
private readonly paymentService: IPaymentService;
|
|
44
|
+
|
|
45
|
+
constructor({
|
|
46
|
+
logger,
|
|
47
|
+
subscriptionService,
|
|
48
|
+
subscriptionPlanService,
|
|
49
|
+
paymentService,
|
|
50
|
+
}: {
|
|
51
|
+
logger: Logger;
|
|
52
|
+
subscriptionService?: ISubscriptionService;
|
|
53
|
+
subscriptionPlanService?: ISubscriptionPlanService;
|
|
54
|
+
paymentService?: IPaymentService;
|
|
55
|
+
}) {
|
|
56
|
+
this.logger = logger;
|
|
57
|
+
this.subscriptionService = subscriptionService || new SubscriptionService({ logger });
|
|
58
|
+
this.subscriptionPlanService = subscriptionPlanService || new SubscriptionPlanService({ logger });
|
|
59
|
+
this.paymentService = paymentService || new PaymentService({ logger });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Preview upgrade cost without initiating payment.
|
|
64
|
+
* Calculates the remaining value of the current subscription and the cost to upgrade.
|
|
65
|
+
*/
|
|
66
|
+
public async getUpgradePreview(params: {
|
|
67
|
+
userId: string;
|
|
68
|
+
platform: string;
|
|
69
|
+
newPlanId: string;
|
|
70
|
+
}): Promise<UpgradePreview> {
|
|
71
|
+
const { userId, platform, newPlanId } = params;
|
|
72
|
+
|
|
73
|
+
// Get active subscription
|
|
74
|
+
const subscription = await this.subscriptionService.getByUser({
|
|
75
|
+
userId,
|
|
76
|
+
platform,
|
|
77
|
+
status: 'active',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!subscription) {
|
|
81
|
+
throw new SubscriptionChangeError(
|
|
82
|
+
SubscriptionChangeErrorCodes.NO_ACTIVE_SUBSCRIPTION,
|
|
83
|
+
'No active subscription found for user',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if subscription has expired
|
|
88
|
+
const now = moment.utc();
|
|
89
|
+
const expiresAt = moment.utc(subscription.expiresAt);
|
|
90
|
+
if (expiresAt.isBefore(now)) {
|
|
91
|
+
throw new SubscriptionChangeError(
|
|
92
|
+
SubscriptionChangeErrorCodes.SUBSCRIPTION_EXPIRED,
|
|
93
|
+
'Current subscription has expired',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get current and new plans
|
|
98
|
+
const [currentPlan, newPlan] = await Promise.all([
|
|
99
|
+
this.subscriptionPlanService.getById(subscription.planId),
|
|
100
|
+
this.subscriptionPlanService.getById(newPlanId),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
if (!currentPlan) {
|
|
104
|
+
throw new SubscriptionChangeError(
|
|
105
|
+
SubscriptionChangeErrorCodes.PLAN_NOT_FOUND,
|
|
106
|
+
`Current plan not found: ${subscription.planId}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!newPlan) {
|
|
111
|
+
throw new SubscriptionChangeError(
|
|
112
|
+
SubscriptionChangeErrorCodes.PLAN_NOT_FOUND,
|
|
113
|
+
`New plan not found: ${newPlanId}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if same plan
|
|
118
|
+
if (currentPlan.id === newPlan.id) {
|
|
119
|
+
throw new SubscriptionChangeError(
|
|
120
|
+
SubscriptionChangeErrorCodes.SAME_PLAN,
|
|
121
|
+
'Cannot upgrade to the same plan',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check currency match
|
|
126
|
+
if (currentPlan.currency !== newPlan.currency) {
|
|
127
|
+
throw new SubscriptionChangeError(
|
|
128
|
+
SubscriptionChangeErrorCodes.CURRENCY_MISMATCH,
|
|
129
|
+
'Cannot change between plans with different currencies',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if this is actually an upgrade (new plan must be more expensive)
|
|
134
|
+
if (newPlan.amount <= currentPlan.amount) {
|
|
135
|
+
throw new SubscriptionChangeError(
|
|
136
|
+
SubscriptionChangeErrorCodes.DOWNGRADE_NOT_ALLOWED,
|
|
137
|
+
'New plan must be more expensive than current plan. Use downgrade instead.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Calculate remaining value
|
|
142
|
+
const remainingValue = this.calculateRemainingValue(subscription, currentPlan);
|
|
143
|
+
const remainingDays = this.calculateRemainingDays(subscription);
|
|
144
|
+
|
|
145
|
+
// Calculate upgrade cost
|
|
146
|
+
const upgradeCost = Math.max(0, Math.round((newPlan.amount - remainingValue) * 100) / 100);
|
|
147
|
+
|
|
148
|
+
// Validate upgrade cost
|
|
149
|
+
if (upgradeCost <= 0) {
|
|
150
|
+
throw new SubscriptionChangeError(
|
|
151
|
+
SubscriptionChangeErrorCodes.INVALID_UPGRADE_COST,
|
|
152
|
+
'Upgrade cost must be greater than 0',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
currentPlan,
|
|
158
|
+
newPlan,
|
|
159
|
+
remainingDays,
|
|
160
|
+
remainingValue: Math.round(remainingValue * 100) / 100,
|
|
161
|
+
upgradeCost,
|
|
162
|
+
currency: currentPlan.currency,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Initiate upgrade - creates payment for the difference.
|
|
168
|
+
* Creates a recurring payment for the new plan.
|
|
169
|
+
*/
|
|
170
|
+
public async initiateUpgrade(params: {
|
|
171
|
+
userId: string;
|
|
172
|
+
platform: string;
|
|
173
|
+
newPlanId: string;
|
|
174
|
+
productName: string;
|
|
175
|
+
language?: string;
|
|
176
|
+
}): Promise<UpgradeResult> {
|
|
177
|
+
const { userId, platform, newPlanId, productName, language } = params;
|
|
178
|
+
|
|
179
|
+
// Get upgrade preview (validates everything)
|
|
180
|
+
const preview = await this.getUpgradePreview({ userId, platform, newPlanId });
|
|
181
|
+
|
|
182
|
+
this.logger.info({
|
|
183
|
+
message: 'Initiating subscription upgrade',
|
|
184
|
+
payload: {
|
|
185
|
+
userId,
|
|
186
|
+
platform,
|
|
187
|
+
currentPlanId: preview.currentPlan.id,
|
|
188
|
+
newPlanId: preview.newPlan.id,
|
|
189
|
+
upgradeCost: preview.upgradeCost,
|
|
190
|
+
currency: preview.currency,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Create upgrade payment intent
|
|
195
|
+
const payment = await this.paymentService.createUpgradePaymentIntent({
|
|
196
|
+
userId,
|
|
197
|
+
platform,
|
|
198
|
+
currentPlanId: preview.currentPlan.id,
|
|
199
|
+
newPlanId: preview.newPlan.id,
|
|
200
|
+
productName,
|
|
201
|
+
upgradeCost: preview.upgradeCost,
|
|
202
|
+
currency: preview.currency,
|
|
203
|
+
regularMode: preview.newPlan.regularMode,
|
|
204
|
+
regularCount: preview.newPlan.count,
|
|
205
|
+
language,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.logger.info({
|
|
209
|
+
message: 'Upgrade payment intent created',
|
|
210
|
+
payload: {
|
|
211
|
+
userId,
|
|
212
|
+
platform,
|
|
213
|
+
paymentId: payment.id,
|
|
214
|
+
orderReference: payment.orderReference,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
payment,
|
|
220
|
+
paymentUrl: payment.paymentLink,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Simple downgrade - just expire current subscription.
|
|
226
|
+
* User will need to purchase the new subscription themselves.
|
|
227
|
+
*/
|
|
228
|
+
public async downgrade(params: {
|
|
229
|
+
userId: string;
|
|
230
|
+
platform: string;
|
|
231
|
+
}): Promise<void> {
|
|
232
|
+
const { userId, platform } = params;
|
|
233
|
+
|
|
234
|
+
// Get active subscription
|
|
235
|
+
const subscription = await this.subscriptionService.getByUser({
|
|
236
|
+
userId,
|
|
237
|
+
platform,
|
|
238
|
+
status: 'active',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!subscription) {
|
|
242
|
+
throw new SubscriptionChangeError(
|
|
243
|
+
SubscriptionChangeErrorCodes.NO_ACTIVE_SUBSCRIPTION,
|
|
244
|
+
'No active subscription found for user',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.logger.info({
|
|
249
|
+
message: 'Downgrading subscription (setting to expired)',
|
|
250
|
+
payload: {
|
|
251
|
+
userId,
|
|
252
|
+
platform,
|
|
253
|
+
subscriptionId: subscription.id,
|
|
254
|
+
planId: subscription.planId,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Set subscription status to expired
|
|
259
|
+
await this.subscriptionService.updateFieldsById({
|
|
260
|
+
subscriptionId: subscription.id,
|
|
261
|
+
fields: [['status', 'expired']],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
this.logger.info({
|
|
265
|
+
message: 'Subscription downgraded successfully',
|
|
266
|
+
payload: {
|
|
267
|
+
userId,
|
|
268
|
+
platform,
|
|
269
|
+
subscriptionId: subscription.id,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Calculate remaining value of a subscription.
|
|
276
|
+
* Formula: remainingDays * dailyRate
|
|
277
|
+
* Where dailyRate = plan.amount / totalPeriodDays
|
|
278
|
+
*/
|
|
279
|
+
private calculateRemainingValue(
|
|
280
|
+
subscription: SubscriptionEntity,
|
|
281
|
+
plan: SubscriptionPlanEntity,
|
|
282
|
+
): number {
|
|
283
|
+
const remainingDays = this.calculateRemainingDays(subscription);
|
|
284
|
+
const totalPeriodDays = this.getPeriodDays(plan.regularMode, plan.count);
|
|
285
|
+
const dailyRate = plan.amount / totalPeriodDays;
|
|
286
|
+
|
|
287
|
+
return remainingDays * dailyRate;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Calculate remaining days until subscription expires.
|
|
292
|
+
*/
|
|
293
|
+
private calculateRemainingDays(subscription: SubscriptionEntity): number {
|
|
294
|
+
const now = moment.utc();
|
|
295
|
+
const expiresAt = moment.utc(subscription.expiresAt);
|
|
296
|
+
const remainingMs = expiresAt.diff(now);
|
|
297
|
+
|
|
298
|
+
// Convert to days (fractional)
|
|
299
|
+
return Math.max(0, remainingMs / (1000 * 60 * 60 * 24));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get the number of days in a subscription period.
|
|
304
|
+
*/
|
|
305
|
+
private getPeriodDays(regularMode: 'daily' | 'monthly' | 'yearly', count: number): number {
|
|
306
|
+
switch (regularMode) {
|
|
307
|
+
case 'daily':
|
|
308
|
+
return count;
|
|
309
|
+
case 'monthly':
|
|
310
|
+
return count * 30; // Approximate
|
|
311
|
+
case 'yearly':
|
|
312
|
+
return count * 365; // Approximate
|
|
313
|
+
default:
|
|
314
|
+
return count * 30;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { FieldValue, Firestore, getFirestore, Query, QueryDocumentSnapshot } from 'firebase-admin/firestore';
|
|
2
|
+
import { SubscriptionEntity, SubscriptionFieldPath, ISubscriptionRepository } from './types';
|
|
3
|
+
import { isUndefined } from '../../utils';
|
|
4
|
+
|
|
5
|
+
export type UpdateDBSubscriptionFields = [
|
|
6
|
+
SubscriptionFieldPath,
|
|
7
|
+
FieldValue | string | number | boolean | Date | [] | {},
|
|
8
|
+
][];
|
|
9
|
+
|
|
10
|
+
export class SubscriptionRepository implements ISubscriptionRepository {
|
|
11
|
+
private readonly db: Firestore;
|
|
12
|
+
private readonly collectionName = 'subscriptions';
|
|
13
|
+
|
|
14
|
+
constructor({ db }: { db?: Firestore } = {}) {
|
|
15
|
+
this.db = db || getFirestore();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public async getByUser(params: {
|
|
19
|
+
userId: string;
|
|
20
|
+
platform: string;
|
|
21
|
+
status?: SubscriptionEntity['status'] | SubscriptionEntity['status'][];
|
|
22
|
+
}): Promise<SubscriptionEntity | null> {
|
|
23
|
+
const { userId, platform } = params;
|
|
24
|
+
|
|
25
|
+
let query = this.db
|
|
26
|
+
.collection(this.collectionName)
|
|
27
|
+
.where('platform', '==', platform)
|
|
28
|
+
.where('userId', '==', userId);
|
|
29
|
+
|
|
30
|
+
if (!isUndefined(params.status)) {
|
|
31
|
+
// Handle status filter
|
|
32
|
+
if (Array.isArray(params.status)) {
|
|
33
|
+
// Multiple statuses - use 'in' operator
|
|
34
|
+
query = query.where('status', 'in', params.status);
|
|
35
|
+
} else {
|
|
36
|
+
// Single status - use '==' operator
|
|
37
|
+
query = query.where('status', '==', params.status);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const querySnapshot = await query.limit(1).get();
|
|
42
|
+
|
|
43
|
+
if (querySnapshot.empty) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const doc = querySnapshot.docs[0];
|
|
48
|
+
return this.mapDocumentToEntity(doc);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async getByStatus(params: {
|
|
52
|
+
status: SubscriptionEntity['status'] | SubscriptionEntity['status'][];
|
|
53
|
+
}): Promise<SubscriptionEntity[]> {
|
|
54
|
+
const { status } = params;
|
|
55
|
+
|
|
56
|
+
let query: Query;
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(status)) {
|
|
59
|
+
query = await this.db.collection(this.collectionName).where('status', 'in', status);
|
|
60
|
+
} else {
|
|
61
|
+
query = await this.db.collection(this.collectionName).where('status', '==', status);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const querySnapshot = await query.get();
|
|
65
|
+
|
|
66
|
+
if (querySnapshot.empty) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async create(params: {
|
|
74
|
+
userId: string;
|
|
75
|
+
platform: string;
|
|
76
|
+
planId: string;
|
|
77
|
+
expiresAt: string;
|
|
78
|
+
startedAt: string;
|
|
79
|
+
}): Promise<SubscriptionEntity> {
|
|
80
|
+
const { userId, platform, planId, expiresAt, startedAt } = params;
|
|
81
|
+
|
|
82
|
+
const docRef = this.db.collection(this.collectionName).doc();
|
|
83
|
+
|
|
84
|
+
const subscriptionEntity: SubscriptionEntity = {
|
|
85
|
+
id: docRef.id,
|
|
86
|
+
userId,
|
|
87
|
+
platform,
|
|
88
|
+
planId,
|
|
89
|
+
expiresAt,
|
|
90
|
+
startedAt,
|
|
91
|
+
status: 'active',
|
|
92
|
+
provider: 'wayforpay',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
docRef.set(subscriptionEntity);
|
|
96
|
+
|
|
97
|
+
return subscriptionEntity;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public async updateFieldsByUserId(params: {
|
|
101
|
+
userId: string;
|
|
102
|
+
platform: string;
|
|
103
|
+
fields: UpdateDBSubscriptionFields;
|
|
104
|
+
}): Promise<void> {
|
|
105
|
+
const { userId, platform, fields } = params;
|
|
106
|
+
|
|
107
|
+
const updateObject = this.buildUpdateObject(fields);
|
|
108
|
+
|
|
109
|
+
const querySnapshot = await this.db
|
|
110
|
+
.collection(this.collectionName)
|
|
111
|
+
.where('platform', '==', platform)
|
|
112
|
+
.where('userId', '==', userId)
|
|
113
|
+
.limit(1)
|
|
114
|
+
.get();
|
|
115
|
+
|
|
116
|
+
if (querySnapshot.empty) {
|
|
117
|
+
throw new Error(`Subscription not found for userId: ${userId}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await querySnapshot.docs[0].ref.update(updateObject);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public async getExpiredActiveSubscriptions(): Promise<SubscriptionEntity[]> {
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
|
|
126
|
+
const querySnapshot = await this.db
|
|
127
|
+
.collection(this.collectionName)
|
|
128
|
+
.where('status', '==', 'active')
|
|
129
|
+
.where('expiresAt', '<', now)
|
|
130
|
+
.get();
|
|
131
|
+
|
|
132
|
+
if (querySnapshot.empty) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public async updateFieldsById(params: {
|
|
140
|
+
subscriptionId: string;
|
|
141
|
+
fields: UpdateDBSubscriptionFields;
|
|
142
|
+
}): Promise<void> {
|
|
143
|
+
const { subscriptionId, fields } = params;
|
|
144
|
+
|
|
145
|
+
const updateObject = this.buildUpdateObject(fields);
|
|
146
|
+
|
|
147
|
+
await this.db.collection(this.collectionName).doc(subscriptionId).update(updateObject);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private mapDocumentToEntity(doc: QueryDocumentSnapshot): SubscriptionEntity {
|
|
151
|
+
return {
|
|
152
|
+
id: doc.id,
|
|
153
|
+
...doc.data(),
|
|
154
|
+
} as SubscriptionEntity;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public async activateSubscription(params: Omit<SubscriptionEntity, 'id' | 'status'>): Promise<void> {
|
|
158
|
+
const { userId, platform } = params;
|
|
159
|
+
return await this.db.runTransaction(async (tx) => {
|
|
160
|
+
// Get user document
|
|
161
|
+
const userSnapshot = await tx.get(this.db.collection(`platform/${platform}/users`).doc(userId));
|
|
162
|
+
|
|
163
|
+
if (!userSnapshot.exists) {
|
|
164
|
+
throw new Error(`User not found: ${userId}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const docRef = this.db.collection(this.collectionName).doc();
|
|
168
|
+
const newSubscription: SubscriptionEntity = {
|
|
169
|
+
...params,
|
|
170
|
+
id: docRef.id,
|
|
171
|
+
status: 'active',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
await tx.create(docRef, newSubscription);
|
|
175
|
+
|
|
176
|
+
// Update user document
|
|
177
|
+
await tx.update(userSnapshot.ref, {
|
|
178
|
+
'subscription.isActive': true,
|
|
179
|
+
'subscription.isTrial': false,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async deactivateSubscription({ id }: Pick<SubscriptionEntity, 'id'>): Promise<void> {
|
|
185
|
+
return await this.db.runTransaction(async (tx) => {
|
|
186
|
+
const subscriptionSnapshot = await tx.get(this.db.collection(this.collectionName).doc(id));
|
|
187
|
+
|
|
188
|
+
if (!subscriptionSnapshot.exists) {
|
|
189
|
+
throw new Error(`Subscription not found for id: ${id}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const subscription = subscriptionSnapshot.data() as SubscriptionEntity;
|
|
193
|
+
const userSnapshot = await tx.get(
|
|
194
|
+
this.db.collection(`platform/${subscription.platform}/users`).doc(subscription.userId),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!userSnapshot.exists) {
|
|
198
|
+
throw new Error(`User not found: ${subscription.userId}; platform: ${subscription.platform}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const newSubscriptionData: Pick<SubscriptionEntity, 'status'> = {
|
|
202
|
+
status: 'expired',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
await tx.update(subscriptionSnapshot.ref, newSubscriptionData);
|
|
206
|
+
await tx.update(userSnapshot.ref, {
|
|
207
|
+
'subscription.isActive': false,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public async cancelSubscription({ id }: Pick<SubscriptionEntity, 'id'>): Promise<void> {
|
|
213
|
+
return await this.db.runTransaction(async (tx) => {
|
|
214
|
+
const subscriptionSnapshot = await tx.get(this.db.collection(this.collectionName).doc(id));
|
|
215
|
+
|
|
216
|
+
if (!subscriptionSnapshot.exists) {
|
|
217
|
+
throw new Error(`Subscription not found for id: ${id}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const newSubscriptionData: Pick<SubscriptionEntity, 'status'> = {
|
|
221
|
+
status: 'cancelled',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
await tx.update(subscriptionSnapshot.ref, newSubscriptionData);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public async renewSubscription(
|
|
229
|
+
params: Pick<SubscriptionEntity, 'userId' | 'platform' | 'expiresAt'>,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const { userId, platform } = params;
|
|
232
|
+
return await this.db.runTransaction(async (tx) => {
|
|
233
|
+
// Get user document
|
|
234
|
+
const userSnapshot = await tx.get(this.db.collection(`platform/${platform}/users`).doc(userId));
|
|
235
|
+
// Get subscription document
|
|
236
|
+
const subscriptionSnapshot = await tx.get(
|
|
237
|
+
this.db
|
|
238
|
+
.collection(this.collectionName)
|
|
239
|
+
.where('userId', '==', userId)
|
|
240
|
+
.where('platform', '==', platform)
|
|
241
|
+
.where('status', '==', 'active')
|
|
242
|
+
.limit(1),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (subscriptionSnapshot.empty) {
|
|
246
|
+
throw new Error(`Subscription not found for userId: ${userId}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!userSnapshot.exists) {
|
|
250
|
+
throw new Error(`User not found: ${userId}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const updateObject: Pick<SubscriptionEntity, 'status' | 'expiresAt'> = {
|
|
254
|
+
status: 'active',
|
|
255
|
+
expiresAt: params.expiresAt,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
await tx.update(subscriptionSnapshot.docs[0].ref, updateObject);
|
|
259
|
+
|
|
260
|
+
// Update user document
|
|
261
|
+
await tx.update(userSnapshot.ref, {
|
|
262
|
+
'subscription.isActive': true,
|
|
263
|
+
'subscription.isTrial': false,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private buildUpdateObject(fields: UpdateDBSubscriptionFields): Record<string, any> {
|
|
269
|
+
const updateObject: Record<string, any> = {};
|
|
270
|
+
|
|
271
|
+
fields.forEach(([fieldPath, value]) => {
|
|
272
|
+
updateObject[fieldPath] = value;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return updateObject;
|
|
276
|
+
}
|
|
277
|
+
}
|