@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,812 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import type { ResponseTransactionStatus } from '@misterhomer1992/wayforpay-api';
|
|
3
|
+
import { Logger } from '../logger/types';
|
|
4
|
+
|
|
5
|
+
const APPROVED_STATUS: ResponseTransactionStatus = 'Approved';
|
|
6
|
+
const DECLINED_STATUS: ResponseTransactionStatus = 'Declined';
|
|
7
|
+
const EXPIRED_STATUS: ResponseTransactionStatus = 'Expired';
|
|
8
|
+
import { WayForPayService } from './wayforpay.service';
|
|
9
|
+
import { PaymentService } from './service';
|
|
10
|
+
import { InvoiceService } from '../invoice/service';
|
|
11
|
+
import { SubscriptionService } from '../subscription/service';
|
|
12
|
+
import { SubscriptionPlanService } from '../subscriptionPlan/service';
|
|
13
|
+
import { TokenPackService } from '../tokenPack/service';
|
|
14
|
+
import {
|
|
15
|
+
parseOrderReference,
|
|
16
|
+
isSubscriptionOrderReference,
|
|
17
|
+
isTokenPackOrderReference,
|
|
18
|
+
isUpgradeOrderReference,
|
|
19
|
+
getOrderReferenceUserId,
|
|
20
|
+
getOrderReferenceItemId,
|
|
21
|
+
parseUpgradeOrderReference,
|
|
22
|
+
} from './utils';
|
|
23
|
+
import type { WayForPayCallbackData, IWayForPayService } from './wayforpay.types';
|
|
24
|
+
import type { IPaymentService } from './types';
|
|
25
|
+
import type { IInvoiceService } from '../invoice/types';
|
|
26
|
+
import type { ISubscriptionService, SubscriptionEntity } from '../subscription/types';
|
|
27
|
+
import type { ISubscriptionPlanService } from '../subscriptionPlan/types';
|
|
28
|
+
import type { ITokenPackService } from '../tokenPack/types';
|
|
29
|
+
import type { WebhookProcessingResult, IWebhookHandlerService } from './webhook.types';
|
|
30
|
+
import { DEFAULT_TOKEN_PACK_PLANS } from '../token/const';
|
|
31
|
+
import type { TokenPackPlan } from '../token/types';
|
|
32
|
+
import { ConfigurationManager, GracePeriodConfig, DEFAULT_GRACE_PERIOD_CONFIG } from '../../config';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* WebhookHandlerService processes payment confirmation webhooks from WayForPay.
|
|
36
|
+
* It handles signature verification, payment status updates, subscription activation,
|
|
37
|
+
* and user token balance initialization.
|
|
38
|
+
*/
|
|
39
|
+
export class WebhookHandlerService implements IWebhookHandlerService {
|
|
40
|
+
private readonly logger: Logger;
|
|
41
|
+
private readonly wayForPayService: IWayForPayService;
|
|
42
|
+
private readonly paymentService: IPaymentService;
|
|
43
|
+
private readonly invoiceService: IInvoiceService;
|
|
44
|
+
private readonly subscriptionService: ISubscriptionService;
|
|
45
|
+
private readonly subscriptionPlanService: ISubscriptionPlanService;
|
|
46
|
+
private readonly tokenPackService: ITokenPackService;
|
|
47
|
+
private readonly tokenPackPlans: TokenPackPlan[];
|
|
48
|
+
private readonly gracePeriodConfig: GracePeriodConfig;
|
|
49
|
+
|
|
50
|
+
constructor({
|
|
51
|
+
logger,
|
|
52
|
+
wayForPayService,
|
|
53
|
+
paymentService,
|
|
54
|
+
invoiceService,
|
|
55
|
+
subscriptionService,
|
|
56
|
+
subscriptionPlanService,
|
|
57
|
+
tokenPackService,
|
|
58
|
+
tokenPackPlans,
|
|
59
|
+
gracePeriodConfig,
|
|
60
|
+
}: {
|
|
61
|
+
logger: Logger;
|
|
62
|
+
wayForPayService?: IWayForPayService;
|
|
63
|
+
paymentService?: IPaymentService;
|
|
64
|
+
invoiceService?: IInvoiceService;
|
|
65
|
+
subscriptionService?: ISubscriptionService;
|
|
66
|
+
subscriptionPlanService?: ISubscriptionPlanService;
|
|
67
|
+
tokenPackService?: ITokenPackService;
|
|
68
|
+
tokenPackPlans?: TokenPackPlan[];
|
|
69
|
+
gracePeriodConfig?: GracePeriodConfig;
|
|
70
|
+
}) {
|
|
71
|
+
this.logger = logger;
|
|
72
|
+
this.wayForPayService = wayForPayService || new WayForPayService({ logger });
|
|
73
|
+
this.paymentService = paymentService || new PaymentService({ logger });
|
|
74
|
+
this.invoiceService = invoiceService || new InvoiceService({ logger });
|
|
75
|
+
this.subscriptionService = subscriptionService || new SubscriptionService({ logger });
|
|
76
|
+
this.subscriptionPlanService = subscriptionPlanService || new SubscriptionPlanService({ logger });
|
|
77
|
+
this.tokenPackService = tokenPackService || new TokenPackService({ logger });
|
|
78
|
+
this.tokenPackPlans = tokenPackPlans || DEFAULT_TOKEN_PACK_PLANS;
|
|
79
|
+
this.gracePeriodConfig = gracePeriodConfig || this.loadGracePeriodConfig();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private loadGracePeriodConfig(): GracePeriodConfig {
|
|
83
|
+
try {
|
|
84
|
+
const configManager = ConfigurationManager.getInstance();
|
|
85
|
+
return configManager.getGracePeriodConfig();
|
|
86
|
+
} catch {
|
|
87
|
+
return DEFAULT_GRACE_PERIOD_CONFIG;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Processes a payment confirmation webhook from WayForPay.
|
|
93
|
+
* This method handles the complete webhook flow:
|
|
94
|
+
* 1. Verifies the signature to validate request authenticity
|
|
95
|
+
* 2. Creates an invoice record
|
|
96
|
+
* 3. Updates payment status
|
|
97
|
+
* 4. Activates subscription or renews existing one
|
|
98
|
+
* 5. Initializes user token balance based on plan
|
|
99
|
+
*
|
|
100
|
+
* @param callbackData - The callback data from WayForPay
|
|
101
|
+
* @returns Processing result with response to send back to WayForPay
|
|
102
|
+
*/
|
|
103
|
+
public async processPaymentWebhook(callbackData: WayForPayCallbackData): Promise<WebhookProcessingResult> {
|
|
104
|
+
const { orderReference, transactionStatus } = callbackData;
|
|
105
|
+
|
|
106
|
+
this.logger.info({
|
|
107
|
+
message: 'Processing WayForPay webhook',
|
|
108
|
+
payload: {
|
|
109
|
+
orderReference,
|
|
110
|
+
transactionStatus,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Step 1: Verify signature to validate request authenticity
|
|
116
|
+
const signatureResult = this.wayForPayService.verifyCallbackSignature(callbackData);
|
|
117
|
+
|
|
118
|
+
if (!signatureResult.isValid) {
|
|
119
|
+
this.logger.warning({
|
|
120
|
+
message: 'Invalid webhook signature',
|
|
121
|
+
payload: {
|
|
122
|
+
orderReference,
|
|
123
|
+
error: signatureResult.error,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
response: this.wayForPayService.buildWebhookResponse(orderReference, 'decline'),
|
|
130
|
+
error: signatureResult.error || 'Invalid signature',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Step 2: Check if this is an approved transaction
|
|
135
|
+
if (transactionStatus !== APPROVED_STATUS) {
|
|
136
|
+
this.logger.info({
|
|
137
|
+
message: 'Webhook received for non-approved transaction',
|
|
138
|
+
payload: {
|
|
139
|
+
orderReference,
|
|
140
|
+
transactionStatus,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Handle declined or other statuses
|
|
145
|
+
if (transactionStatus === DECLINED_STATUS || transactionStatus === EXPIRED_STATUS) {
|
|
146
|
+
await this.handleFailedPayment(callbackData);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
response: this.wayForPayService.buildWebhookResponse(orderReference, 'accept'),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Step 3: Process based on payment type
|
|
156
|
+
if (isUpgradeOrderReference(orderReference)) {
|
|
157
|
+
// Process upgrade payment
|
|
158
|
+
await this.processApprovedUpgradePayment(callbackData);
|
|
159
|
+
} else if (isSubscriptionOrderReference(orderReference)) {
|
|
160
|
+
// Process subscription payment
|
|
161
|
+
await this.processApprovedSubscriptionPayment(callbackData);
|
|
162
|
+
} else if (isTokenPackOrderReference(orderReference)) {
|
|
163
|
+
// Process token pack payment
|
|
164
|
+
await this.processApprovedTokenPackPayment(callbackData);
|
|
165
|
+
} else {
|
|
166
|
+
this.logger.info({
|
|
167
|
+
message: 'Webhook received for unknown payment type',
|
|
168
|
+
payload: { orderReference },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
response: this.wayForPayService.buildWebhookResponse(orderReference, 'accept'),
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.logger.error({
|
|
178
|
+
message: 'Error processing webhook',
|
|
179
|
+
payload: {
|
|
180
|
+
orderReference,
|
|
181
|
+
error: JSON.stringify(error),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
response: this.wayForPayService.buildWebhookResponse(orderReference, 'decline'),
|
|
188
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Processes an approved subscription payment.
|
|
195
|
+
* Creates invoice, updates payment status, activates subscription,
|
|
196
|
+
* and initializes token balance.
|
|
197
|
+
*/
|
|
198
|
+
private async processApprovedSubscriptionPayment(callbackData: WayForPayCallbackData): Promise<void> {
|
|
199
|
+
const { orderReference, merchantAccount, merchantSignature, reasonCode, reason, amount, currency } =
|
|
200
|
+
callbackData;
|
|
201
|
+
|
|
202
|
+
// Parse order reference to extract user and plan info
|
|
203
|
+
const parsedReference = parseOrderReference(orderReference);
|
|
204
|
+
if (!parsedReference) {
|
|
205
|
+
throw new Error(`Invalid order reference format: ${orderReference}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const userId = getOrderReferenceUserId(orderReference);
|
|
209
|
+
const planId = getOrderReferenceItemId(orderReference);
|
|
210
|
+
const { platform } = parsedReference;
|
|
211
|
+
|
|
212
|
+
if (!userId || !planId) {
|
|
213
|
+
throw new Error(`Could not extract userId or planId from order reference: ${orderReference}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for duplicate webhook (idempotency)
|
|
217
|
+
const existingInvoice = await this.invoiceService.getByOrderReference(orderReference);
|
|
218
|
+
if (existingInvoice) {
|
|
219
|
+
this.logger.info({
|
|
220
|
+
message: 'Duplicate webhook received, already processed',
|
|
221
|
+
payload: { orderReference },
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Create invoice record
|
|
227
|
+
// reasonCode from WayForPay is a string like '1100', convert to number for storage
|
|
228
|
+
const reasonCodeNumber = typeof reasonCode === 'string' ? parseInt(reasonCode, 10) : Number(reasonCode);
|
|
229
|
+
const invoice = await this.invoiceService.create({
|
|
230
|
+
merchantAccount,
|
|
231
|
+
orderReference,
|
|
232
|
+
merchantSignature,
|
|
233
|
+
reasonCode: reasonCodeNumber,
|
|
234
|
+
reason,
|
|
235
|
+
createdDate: moment.unix(callbackData.createdDate).toISOString(),
|
|
236
|
+
processingDate: moment.unix(callbackData.processingDate).toISOString(),
|
|
237
|
+
currency,
|
|
238
|
+
amount,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
this.logger.info({
|
|
242
|
+
message: 'Invoice created',
|
|
243
|
+
payload: { invoiceId: invoice.id, orderReference },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Update payment status to completed
|
|
247
|
+
const payment = await this.paymentService.getByOrderReference(orderReference);
|
|
248
|
+
if (payment && payment.id) {
|
|
249
|
+
await this.paymentService.changeStatus({
|
|
250
|
+
id: payment.id,
|
|
251
|
+
status: 'completed',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this.logger.info({
|
|
255
|
+
message: 'Payment status updated to completed',
|
|
256
|
+
payload: { paymentId: payment.id, orderReference },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get subscription plan to calculate expiration and tokens
|
|
261
|
+
const subscriptionPlan = await this.subscriptionPlanService.getById(planId);
|
|
262
|
+
if (!subscriptionPlan) {
|
|
263
|
+
throw new Error(`Subscription plan not found: ${planId}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Calculate subscription expiration date
|
|
267
|
+
const startedAt = moment.utc().toISOString();
|
|
268
|
+
const expiresAt = this.calculateExpirationDate(subscriptionPlan.regularMode, subscriptionPlan.count);
|
|
269
|
+
|
|
270
|
+
// Check for existing active subscription
|
|
271
|
+
const existingSubscription = await this.subscriptionService.getByUser({
|
|
272
|
+
userId,
|
|
273
|
+
platform,
|
|
274
|
+
status: 'active',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (existingSubscription) {
|
|
278
|
+
// Renew existing subscription
|
|
279
|
+
await this.renewSubscription(existingSubscription, expiresAt);
|
|
280
|
+
} else {
|
|
281
|
+
// Activate new subscription
|
|
282
|
+
await this.activateNewSubscription({
|
|
283
|
+
userId,
|
|
284
|
+
platform,
|
|
285
|
+
planId,
|
|
286
|
+
startedAt,
|
|
287
|
+
expiresAt,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Initialize user token balance based on plan credits
|
|
292
|
+
await this.initializeTokenBalance({
|
|
293
|
+
userId,
|
|
294
|
+
platform,
|
|
295
|
+
planId,
|
|
296
|
+
tokens: subscriptionPlan.credits,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.logger.info({
|
|
300
|
+
message: 'Subscription activated successfully',
|
|
301
|
+
payload: {
|
|
302
|
+
userId,
|
|
303
|
+
platform,
|
|
304
|
+
planId,
|
|
305
|
+
expiresAt,
|
|
306
|
+
tokens: subscriptionPlan.credits,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Processes an approved token pack payment.
|
|
313
|
+
* Creates invoice, updates payment status, and adds tokens to user balance.
|
|
314
|
+
*/
|
|
315
|
+
private async processApprovedTokenPackPayment(callbackData: WayForPayCallbackData): Promise<void> {
|
|
316
|
+
const { orderReference, merchantAccount, merchantSignature, reasonCode, reason, amount, currency } =
|
|
317
|
+
callbackData;
|
|
318
|
+
|
|
319
|
+
// Parse order reference to extract user and pack info
|
|
320
|
+
const parsedReference = parseOrderReference(orderReference);
|
|
321
|
+
if (!parsedReference) {
|
|
322
|
+
throw new Error(`Invalid order reference format: ${orderReference}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const userId = getOrderReferenceUserId(orderReference);
|
|
326
|
+
const packId = getOrderReferenceItemId(orderReference);
|
|
327
|
+
const { platform } = parsedReference;
|
|
328
|
+
|
|
329
|
+
if (!userId || !packId) {
|
|
330
|
+
throw new Error(`Could not extract userId or packId from order reference: ${orderReference}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check for duplicate webhook (idempotency)
|
|
334
|
+
const existingInvoice = await this.invoiceService.getByOrderReference(orderReference);
|
|
335
|
+
if (existingInvoice) {
|
|
336
|
+
this.logger.info({
|
|
337
|
+
message: 'Duplicate webhook received for token pack, already processed',
|
|
338
|
+
payload: { orderReference },
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create invoice record
|
|
344
|
+
const reasonCodeNumber = typeof reasonCode === 'string' ? parseInt(reasonCode, 10) : Number(reasonCode);
|
|
345
|
+
const invoice = await this.invoiceService.create({
|
|
346
|
+
merchantAccount,
|
|
347
|
+
orderReference,
|
|
348
|
+
merchantSignature,
|
|
349
|
+
reasonCode: reasonCodeNumber,
|
|
350
|
+
reason,
|
|
351
|
+
createdDate: moment.unix(callbackData.createdDate).toISOString(),
|
|
352
|
+
processingDate: moment.unix(callbackData.processingDate).toISOString(),
|
|
353
|
+
currency,
|
|
354
|
+
amount,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
this.logger.info({
|
|
358
|
+
message: 'Invoice created for token pack',
|
|
359
|
+
payload: { invoiceId: invoice.id, orderReference },
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Update payment status to completed
|
|
363
|
+
const payment = await this.paymentService.getByOrderReference(orderReference);
|
|
364
|
+
if (payment && payment.id) {
|
|
365
|
+
await this.paymentService.changeStatus({
|
|
366
|
+
id: payment.id,
|
|
367
|
+
status: 'completed',
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
this.logger.info({
|
|
371
|
+
message: 'Token pack payment status updated to completed',
|
|
372
|
+
payload: { paymentId: payment.id, orderReference },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Get token pack plan to determine token count
|
|
377
|
+
const tokenPackPlan = this.tokenPackPlans.find((p) => p.id === packId);
|
|
378
|
+
if (!tokenPackPlan) {
|
|
379
|
+
throw new Error(`Token pack plan not found: ${packId}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Create token pack for user
|
|
383
|
+
await this.tokenPackService.create({
|
|
384
|
+
userId,
|
|
385
|
+
platform,
|
|
386
|
+
packId,
|
|
387
|
+
tokens: tokenPackPlan.tokens,
|
|
388
|
+
purchasedAt: moment.utc().toISOString(),
|
|
389
|
+
provider: 'wayforpay',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this.logger.info({
|
|
393
|
+
message: 'Token pack activated successfully',
|
|
394
|
+
payload: {
|
|
395
|
+
userId,
|
|
396
|
+
platform,
|
|
397
|
+
packId,
|
|
398
|
+
tokens: tokenPackPlan.tokens,
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Processes an approved subscription upgrade payment.
|
|
405
|
+
* Updates subscription's planId, resets expiresAt, and adds new plan's tokens to existing balance.
|
|
406
|
+
*/
|
|
407
|
+
private async processApprovedUpgradePayment(callbackData: WayForPayCallbackData): Promise<void> {
|
|
408
|
+
const { orderReference, merchantAccount, merchantSignature, reasonCode, reason, amount, currency } =
|
|
409
|
+
callbackData;
|
|
410
|
+
|
|
411
|
+
// Parse upgrade order reference to extract both plan IDs
|
|
412
|
+
const upgradeParsed = parseUpgradeOrderReference(orderReference);
|
|
413
|
+
if (!upgradeParsed) {
|
|
414
|
+
throw new Error(`Invalid upgrade order reference format: ${orderReference}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const { userId, platform, currentPlanId, newPlanId } = upgradeParsed;
|
|
418
|
+
|
|
419
|
+
if (!userId || !currentPlanId || !newPlanId) {
|
|
420
|
+
throw new Error(`Could not extract userId or plan IDs from upgrade order reference: ${orderReference}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check for duplicate webhook (idempotency)
|
|
424
|
+
const existingInvoice = await this.invoiceService.getByOrderReference(orderReference);
|
|
425
|
+
if (existingInvoice) {
|
|
426
|
+
this.logger.info({
|
|
427
|
+
message: 'Duplicate webhook received for upgrade, already processed',
|
|
428
|
+
payload: { orderReference },
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Create invoice record
|
|
434
|
+
const reasonCodeNumber = typeof reasonCode === 'string' ? parseInt(reasonCode, 10) : Number(reasonCode);
|
|
435
|
+
const invoice = await this.invoiceService.create({
|
|
436
|
+
merchantAccount,
|
|
437
|
+
orderReference,
|
|
438
|
+
merchantSignature,
|
|
439
|
+
reasonCode: reasonCodeNumber,
|
|
440
|
+
reason,
|
|
441
|
+
createdDate: moment.unix(callbackData.createdDate).toISOString(),
|
|
442
|
+
processingDate: moment.unix(callbackData.processingDate).toISOString(),
|
|
443
|
+
currency,
|
|
444
|
+
amount,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
this.logger.info({
|
|
448
|
+
message: 'Invoice created for upgrade',
|
|
449
|
+
payload: { invoiceId: invoice.id, orderReference },
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Update payment status to completed
|
|
453
|
+
const payment = await this.paymentService.getByOrderReference(orderReference);
|
|
454
|
+
if (payment && payment.id) {
|
|
455
|
+
await this.paymentService.changeStatus({
|
|
456
|
+
id: payment.id,
|
|
457
|
+
status: 'completed',
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
this.logger.info({
|
|
461
|
+
message: 'Upgrade payment status updated to completed',
|
|
462
|
+
payload: { paymentId: payment.id, orderReference },
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Get the new subscription plan
|
|
467
|
+
const newPlan = await this.subscriptionPlanService.getById(newPlanId);
|
|
468
|
+
if (!newPlan) {
|
|
469
|
+
throw new Error(`New subscription plan not found: ${newPlanId}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Get existing active subscription
|
|
473
|
+
const existingSubscription = await this.subscriptionService.getByUser({
|
|
474
|
+
userId,
|
|
475
|
+
platform,
|
|
476
|
+
status: 'active',
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (!existingSubscription) {
|
|
480
|
+
this.logger.warning({
|
|
481
|
+
message: 'No active subscription found for upgrade, treating as new subscription',
|
|
482
|
+
payload: { userId, platform, orderReference },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Fall back to creating a new subscription
|
|
486
|
+
const startedAt = moment.utc().toISOString();
|
|
487
|
+
const expiresAt = this.calculateExpirationDate(newPlan.regularMode, newPlan.count);
|
|
488
|
+
|
|
489
|
+
await this.activateNewSubscription({
|
|
490
|
+
userId,
|
|
491
|
+
platform,
|
|
492
|
+
planId: newPlanId,
|
|
493
|
+
startedAt,
|
|
494
|
+
expiresAt,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Initialize token balance with new plan's credits
|
|
498
|
+
await this.initializeTokenBalance({
|
|
499
|
+
userId,
|
|
500
|
+
platform,
|
|
501
|
+
planId: newPlanId,
|
|
502
|
+
tokens: newPlan.credits,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Calculate new expiration date (full new period from now)
|
|
509
|
+
const startedAt = moment.utc().toISOString();
|
|
510
|
+
const expiresAt = this.calculateExpirationDate(newPlan.regularMode, newPlan.count);
|
|
511
|
+
|
|
512
|
+
// Update subscription: change planId, reset expiresAt and startedAt, ensure active status
|
|
513
|
+
await this.subscriptionService.updateFieldsById({
|
|
514
|
+
subscriptionId: existingSubscription.id,
|
|
515
|
+
fields: [
|
|
516
|
+
['planId', newPlanId],
|
|
517
|
+
['expiresAt', expiresAt],
|
|
518
|
+
['startedAt', startedAt],
|
|
519
|
+
['status', 'active'],
|
|
520
|
+
],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
this.logger.info({
|
|
524
|
+
message: 'Subscription upgraded',
|
|
525
|
+
payload: {
|
|
526
|
+
subscriptionId: existingSubscription.id,
|
|
527
|
+
previousPlanId: existingSubscription.planId,
|
|
528
|
+
newPlanId,
|
|
529
|
+
previousExpiresAt: existingSubscription.expiresAt,
|
|
530
|
+
newExpiresAt: expiresAt,
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Add new plan's tokens to existing balance (keep existing + add new)
|
|
535
|
+
await this.initializeTokenBalance({
|
|
536
|
+
userId,
|
|
537
|
+
platform,
|
|
538
|
+
planId: newPlanId,
|
|
539
|
+
tokens: newPlan.credits,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
this.logger.info({
|
|
543
|
+
message: 'Subscription upgrade completed successfully',
|
|
544
|
+
payload: {
|
|
545
|
+
userId,
|
|
546
|
+
platform,
|
|
547
|
+
previousPlanId: currentPlanId,
|
|
548
|
+
newPlanId,
|
|
549
|
+
expiresAt,
|
|
550
|
+
tokensAdded: newPlan.credits,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Activates a new subscription for the user.
|
|
557
|
+
*/
|
|
558
|
+
private async activateNewSubscription(params: {
|
|
559
|
+
userId: string;
|
|
560
|
+
platform: string;
|
|
561
|
+
planId: string;
|
|
562
|
+
startedAt: string;
|
|
563
|
+
expiresAt: string;
|
|
564
|
+
}): Promise<void> {
|
|
565
|
+
const subscriptionData: Omit<SubscriptionEntity, 'id' | 'status'> = {
|
|
566
|
+
userId: params.userId,
|
|
567
|
+
platform: params.platform,
|
|
568
|
+
planId: params.planId,
|
|
569
|
+
startedAt: params.startedAt,
|
|
570
|
+
expiresAt: params.expiresAt,
|
|
571
|
+
provider: 'wayforpay',
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
await this.subscriptionService.activateSubscription(subscriptionData);
|
|
575
|
+
|
|
576
|
+
this.logger.info({
|
|
577
|
+
message: 'New subscription activated',
|
|
578
|
+
payload: {
|
|
579
|
+
userId: params.userId,
|
|
580
|
+
platform: params.platform,
|
|
581
|
+
planId: params.planId,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Renews an existing subscription by extending its expiration date.
|
|
588
|
+
*/
|
|
589
|
+
private async renewSubscription(
|
|
590
|
+
existingSubscription: SubscriptionEntity,
|
|
591
|
+
newExpiresAt: string,
|
|
592
|
+
): Promise<void> {
|
|
593
|
+
// If the subscription hasn't expired yet, extend from current expiration
|
|
594
|
+
const currentExpiration = moment.utc(existingSubscription.expiresAt);
|
|
595
|
+
const now = moment.utc();
|
|
596
|
+
|
|
597
|
+
let extendedExpiresAt: string;
|
|
598
|
+
if (currentExpiration.isAfter(now)) {
|
|
599
|
+
// Extend from current expiration date
|
|
600
|
+
const durationToAdd = moment.utc(newExpiresAt).diff(moment.utc(), 'milliseconds');
|
|
601
|
+
extendedExpiresAt = currentExpiration.add(durationToAdd, 'milliseconds').toISOString();
|
|
602
|
+
} else {
|
|
603
|
+
// Subscription has expired, use the new expiration date
|
|
604
|
+
extendedExpiresAt = newExpiresAt;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await this.subscriptionService.renewSubscription({
|
|
608
|
+
userId: existingSubscription.userId,
|
|
609
|
+
platform: existingSubscription.platform,
|
|
610
|
+
expiresAt: extendedExpiresAt,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
this.logger.info({
|
|
614
|
+
message: 'Subscription renewed',
|
|
615
|
+
payload: {
|
|
616
|
+
subscriptionId: existingSubscription.id,
|
|
617
|
+
previousExpiresAt: existingSubscription.expiresAt,
|
|
618
|
+
newExpiresAt: extendedExpiresAt,
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Initializes user token balance by creating a token pack.
|
|
625
|
+
*/
|
|
626
|
+
private async initializeTokenBalance(params: {
|
|
627
|
+
userId: string;
|
|
628
|
+
platform: string;
|
|
629
|
+
planId: string;
|
|
630
|
+
tokens: number;
|
|
631
|
+
}): Promise<void> {
|
|
632
|
+
const { userId, platform, planId, tokens } = params;
|
|
633
|
+
|
|
634
|
+
await this.tokenPackService.create({
|
|
635
|
+
userId,
|
|
636
|
+
platform,
|
|
637
|
+
packId: planId,
|
|
638
|
+
tokens,
|
|
639
|
+
purchasedAt: moment.utc().toISOString(),
|
|
640
|
+
provider: 'wayforpay',
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
this.logger.info({
|
|
644
|
+
message: 'Token balance initialized',
|
|
645
|
+
payload: {
|
|
646
|
+
userId,
|
|
647
|
+
platform,
|
|
648
|
+
packId: planId,
|
|
649
|
+
tokens,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Calculates the subscription expiration date based on the regular mode.
|
|
656
|
+
*/
|
|
657
|
+
private calculateExpirationDate(
|
|
658
|
+
regularMode: 'daily' | 'monthly' | 'yearly',
|
|
659
|
+
count: number,
|
|
660
|
+
): string {
|
|
661
|
+
const now = moment.utc();
|
|
662
|
+
|
|
663
|
+
switch (regularMode) {
|
|
664
|
+
case 'daily':
|
|
665
|
+
return now.add(count, 'days').toISOString();
|
|
666
|
+
case 'monthly':
|
|
667
|
+
return now.add(count, 'months').toISOString();
|
|
668
|
+
case 'yearly':
|
|
669
|
+
return now.add(count, 'years').toISOString();
|
|
670
|
+
default:
|
|
671
|
+
return now.add(count, 'months').toISOString();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Handles failed or declined payments.
|
|
677
|
+
* For recurring subscription renewals, this triggers deactivation of the subscription.
|
|
678
|
+
*/
|
|
679
|
+
private async handleFailedPayment(callbackData: WayForPayCallbackData): Promise<void> {
|
|
680
|
+
const { orderReference } = callbackData;
|
|
681
|
+
|
|
682
|
+
const payment = await this.paymentService.getByOrderReference(orderReference);
|
|
683
|
+
if (payment && payment.id) {
|
|
684
|
+
await this.paymentService.changeStatus({
|
|
685
|
+
id: payment.id,
|
|
686
|
+
status: 'failed',
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
this.logger.info({
|
|
690
|
+
message: 'Payment status updated to failed',
|
|
691
|
+
payload: {
|
|
692
|
+
paymentId: payment.id,
|
|
693
|
+
orderReference,
|
|
694
|
+
reason: callbackData.reason,
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Handle failed recurring renewal - deactivate subscription
|
|
700
|
+
if (this.isRecurringRenewal(callbackData) && isSubscriptionOrderReference(orderReference)) {
|
|
701
|
+
await this.handleFailedRenewal(callbackData);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Checks if the payment callback is for a recurring renewal payment.
|
|
707
|
+
* WayForPay sets regularOn field for recurring payments.
|
|
708
|
+
*/
|
|
709
|
+
private isRecurringRenewal(callbackData: WayForPayCallbackData): boolean {
|
|
710
|
+
// regularOn is set by WayForPay for recurring payment callbacks
|
|
711
|
+
return !!callbackData.regularOn;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Handles a failed recurring renewal by cancelling or deactivating the subscription.
|
|
716
|
+
* Respects grace period configuration before full deactivation.
|
|
717
|
+
*/
|
|
718
|
+
private async handleFailedRenewal(callbackData: WayForPayCallbackData): Promise<void> {
|
|
719
|
+
const { orderReference, reason, reasonCode } = callbackData;
|
|
720
|
+
|
|
721
|
+
const parsedReference = parseOrderReference(orderReference);
|
|
722
|
+
if (!parsedReference) {
|
|
723
|
+
this.logger.warning({
|
|
724
|
+
message: 'Could not parse order reference for failed renewal',
|
|
725
|
+
payload: { orderReference },
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const userId = getOrderReferenceUserId(orderReference);
|
|
731
|
+
const { platform } = parsedReference;
|
|
732
|
+
|
|
733
|
+
if (!userId) {
|
|
734
|
+
this.logger.warning({
|
|
735
|
+
message: 'Could not extract userId from order reference for failed renewal',
|
|
736
|
+
payload: { orderReference },
|
|
737
|
+
});
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Find active subscription for user
|
|
742
|
+
const existingSubscription = await this.subscriptionService.getByUser({
|
|
743
|
+
userId,
|
|
744
|
+
platform,
|
|
745
|
+
status: 'active',
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (!existingSubscription) {
|
|
749
|
+
this.logger.info({
|
|
750
|
+
message: 'No active subscription found for failed renewal',
|
|
751
|
+
payload: { userId, platform, orderReference },
|
|
752
|
+
});
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Check if subscription has expired past the grace period
|
|
757
|
+
const now = moment.utc();
|
|
758
|
+
const expiresAt = moment.utc(existingSubscription.expiresAt);
|
|
759
|
+
const gracePeriodMs = this.gracePeriodConfig.enabled ? this.gracePeriodConfig.durationMs : 0;
|
|
760
|
+
const gracePeriodEnd = expiresAt.clone().add(gracePeriodMs, 'milliseconds');
|
|
761
|
+
|
|
762
|
+
if (now.isAfter(gracePeriodEnd)) {
|
|
763
|
+
// Subscription has expired past grace period - deactivate it
|
|
764
|
+
await this.subscriptionService.deactivateSubscription({ id: existingSubscription.id });
|
|
765
|
+
|
|
766
|
+
this.logger.info({
|
|
767
|
+
message: 'Subscription deactivated due to failed renewal (past grace period)',
|
|
768
|
+
payload: {
|
|
769
|
+
subscriptionId: existingSubscription.id,
|
|
770
|
+
userId,
|
|
771
|
+
platform,
|
|
772
|
+
orderReference,
|
|
773
|
+
reason,
|
|
774
|
+
reasonCode,
|
|
775
|
+
expiresAt: existingSubscription.expiresAt,
|
|
776
|
+
gracePeriodMs,
|
|
777
|
+
gracePeriodEnd: gracePeriodEnd.toISOString(),
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
} else if (expiresAt.isBefore(now)) {
|
|
781
|
+
// Subscription has expired but within grace period - cancel (mark for future deactivation)
|
|
782
|
+
await this.subscriptionService.cancelSubscription({ id: existingSubscription.id });
|
|
783
|
+
|
|
784
|
+
this.logger.info({
|
|
785
|
+
message: 'Subscription cancelled due to failed renewal (within grace period)',
|
|
786
|
+
payload: {
|
|
787
|
+
subscriptionId: existingSubscription.id,
|
|
788
|
+
userId,
|
|
789
|
+
platform,
|
|
790
|
+
orderReference,
|
|
791
|
+
expiresAt: existingSubscription.expiresAt,
|
|
792
|
+
gracePeriodEnd: gracePeriodEnd.toISOString(),
|
|
793
|
+
reason,
|
|
794
|
+
reasonCode,
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
} else {
|
|
798
|
+
this.logger.info({
|
|
799
|
+
message: 'Failed renewal for subscription that has not yet expired',
|
|
800
|
+
payload: {
|
|
801
|
+
subscriptionId: existingSubscription.id,
|
|
802
|
+
userId,
|
|
803
|
+
platform,
|
|
804
|
+
orderReference,
|
|
805
|
+
expiresAt: existingSubscription.expiresAt,
|
|
806
|
+
reason,
|
|
807
|
+
reasonCode,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|