@classytic/revenue 0.2.3 → 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.
- package/README.md +498 -499
- package/dist/actions-CwG-b7fR.d.ts +519 -0
- package/dist/core/index.d.ts +884 -0
- package/dist/core/index.js +2941 -0
- package/dist/core/index.js.map +1 -0
- package/dist/enums/index.d.ts +130 -0
- package/dist/enums/index.js +167 -0
- package/dist/enums/index.js.map +1 -0
- package/dist/index-BnJWVXuw.d.ts +378 -0
- package/dist/index-ChVD3P9k.d.ts +504 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +4353 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +132 -0
- package/dist/providers/index.js +122 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/retry-80lBCmSe.d.ts +234 -0
- package/dist/schemas/index.d.ts +894 -0
- package/dist/schemas/index.js +524 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/validation.d.ts +309 -0
- package/dist/schemas/validation.js +249 -0
- package/dist/schemas/validation.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1632 -0
- package/dist/services/index.js.map +1 -0
- package/dist/split.enums-DHdM1YAV.d.ts +255 -0
- package/dist/split.schema-BPdFZMbU.d.ts +958 -0
- package/dist/utils/index.d.ts +24 -0
- package/dist/utils/index.js +1067 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +48 -32
- package/core/builder.js +0 -219
- package/core/container.js +0 -119
- package/core/errors.js +0 -262
- package/dist/types/core/builder.d.ts +0 -97
- package/dist/types/core/container.d.ts +0 -57
- package/dist/types/core/errors.d.ts +0 -122
- package/dist/types/enums/escrow.enums.d.ts +0 -24
- package/dist/types/enums/index.d.ts +0 -69
- package/dist/types/enums/monetization.enums.d.ts +0 -6
- package/dist/types/enums/payment.enums.d.ts +0 -16
- package/dist/types/enums/split.enums.d.ts +0 -25
- package/dist/types/enums/subscription.enums.d.ts +0 -15
- package/dist/types/enums/transaction.enums.d.ts +0 -24
- package/dist/types/index.d.ts +0 -22
- package/dist/types/providers/base.d.ts +0 -126
- package/dist/types/schemas/escrow/hold.schema.d.ts +0 -54
- package/dist/types/schemas/escrow/index.d.ts +0 -6
- package/dist/types/schemas/index.d.ts +0 -506
- package/dist/types/schemas/split/index.d.ts +0 -8
- package/dist/types/schemas/split/split.schema.d.ts +0 -142
- package/dist/types/schemas/subscription/index.d.ts +0 -152
- package/dist/types/schemas/subscription/info.schema.d.ts +0 -128
- package/dist/types/schemas/subscription/plan.schema.d.ts +0 -39
- package/dist/types/schemas/transaction/common.schema.d.ts +0 -12
- package/dist/types/schemas/transaction/gateway.schema.d.ts +0 -86
- package/dist/types/schemas/transaction/index.d.ts +0 -202
- package/dist/types/schemas/transaction/payment.schema.d.ts +0 -145
- package/dist/types/services/escrow.service.d.ts +0 -51
- package/dist/types/services/monetization.service.d.ts +0 -193
- package/dist/types/services/payment.service.d.ts +0 -112
- package/dist/types/services/transaction.service.d.ts +0 -40
- package/dist/types/utils/category-resolver.d.ts +0 -46
- package/dist/types/utils/commission-split.d.ts +0 -56
- package/dist/types/utils/commission.d.ts +0 -29
- package/dist/types/utils/hooks.d.ts +0 -17
- package/dist/types/utils/index.d.ts +0 -6
- package/dist/types/utils/logger.d.ts +0 -12
- package/dist/types/utils/subscription/actions.d.ts +0 -28
- package/dist/types/utils/subscription/index.d.ts +0 -2
- package/dist/types/utils/subscription/period.d.ts +0 -47
- package/dist/types/utils/transaction-type.d.ts +0 -102
- package/enums/escrow.enums.js +0 -36
- package/enums/index.d.ts +0 -116
- package/enums/index.js +0 -110
- package/enums/monetization.enums.js +0 -15
- package/enums/payment.enums.js +0 -64
- package/enums/split.enums.js +0 -37
- package/enums/subscription.enums.js +0 -33
- package/enums/transaction.enums.js +0 -69
- package/index.js +0 -76
- package/providers/base.js +0 -162
- package/schemas/escrow/hold.schema.js +0 -62
- package/schemas/escrow/index.js +0 -15
- package/schemas/index.d.ts +0 -33
- package/schemas/index.js +0 -27
- package/schemas/split/index.js +0 -16
- package/schemas/split/split.schema.js +0 -86
- package/schemas/subscription/index.js +0 -17
- package/schemas/subscription/info.schema.js +0 -115
- package/schemas/subscription/plan.schema.js +0 -48
- package/schemas/transaction/common.schema.js +0 -22
- package/schemas/transaction/gateway.schema.js +0 -69
- package/schemas/transaction/index.js +0 -20
- package/schemas/transaction/payment.schema.js +0 -110
- package/services/escrow.service.js +0 -353
- package/services/monetization.service.js +0 -671
- package/services/payment.service.js +0 -517
- package/services/transaction.service.js +0 -142
- package/utils/category-resolver.js +0 -74
- package/utils/commission-split.js +0 -180
- package/utils/commission.js +0 -83
- package/utils/hooks.js +0 -44
- package/utils/index.d.ts +0 -164
- package/utils/index.js +0 -16
- package/utils/logger.js +0 -36
- package/utils/subscription/actions.js +0 -68
- package/utils/subscription/index.js +0 -20
- package/utils/subscription/period.js +0 -123
- package/utils/transaction-type.js +0 -254
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Payment Service
|
|
3
|
-
* @classytic/revenue
|
|
4
|
-
*
|
|
5
|
-
* Framework-agnostic payment verification and management service with DI
|
|
6
|
-
* Handles payment verification, refunds, and status updates
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {Object} PaymentVerifyResult
|
|
11
|
-
* @property {Object} transaction - Updated transaction
|
|
12
|
-
* @property {Object} paymentResult - Payment result from provider
|
|
13
|
-
* @property {string} status - Payment status
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {Object} PaymentRefundResult
|
|
18
|
-
* @property {Object} transaction - Original transaction (updated)
|
|
19
|
-
* @property {Object} refundTransaction - New refund transaction record
|
|
20
|
-
* @property {Object} refundResult - Refund result from provider
|
|
21
|
-
* @property {string} status - Transaction status after refund
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
TransactionNotFoundError,
|
|
26
|
-
ProviderNotFoundError,
|
|
27
|
-
ProviderError,
|
|
28
|
-
AlreadyVerifiedError,
|
|
29
|
-
PaymentVerificationError,
|
|
30
|
-
RefundNotSupportedError,
|
|
31
|
-
RefundError,
|
|
32
|
-
ProviderCapabilityError,
|
|
33
|
-
ValidationError,
|
|
34
|
-
} from '../core/errors.js';
|
|
35
|
-
import { triggerHook } from '../utils/hooks.js';
|
|
36
|
-
import { reverseCommission } from '../utils/commission.js';
|
|
37
|
-
import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Payment Service
|
|
41
|
-
* Uses DI container for all dependencies
|
|
42
|
-
*/
|
|
43
|
-
export class PaymentService {
|
|
44
|
-
constructor(container) {
|
|
45
|
-
this.container = container;
|
|
46
|
-
this.models = container.get('models');
|
|
47
|
-
this.providers = container.get('providers');
|
|
48
|
-
this.config = container.get('config');
|
|
49
|
-
this.hooks = container.get('hooks');
|
|
50
|
-
this.logger = container.get('logger');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Verify a payment
|
|
55
|
-
*
|
|
56
|
-
* @param {String} paymentIntentId - Payment intent ID or transaction ID
|
|
57
|
-
* @param {Object} options - Verification options
|
|
58
|
-
* @param {String} options.verifiedBy - User ID who verified (for manual verification)
|
|
59
|
-
* @returns {Promise<Object>} { transaction, status }
|
|
60
|
-
*/
|
|
61
|
-
async verify(paymentIntentId, options = {}) {
|
|
62
|
-
const { verifiedBy = null } = options;
|
|
63
|
-
|
|
64
|
-
const TransactionModel = this.models.Transaction;
|
|
65
|
-
|
|
66
|
-
// Find transaction by payment intent ID or transaction ID
|
|
67
|
-
let transaction = await TransactionModel.findOne({
|
|
68
|
-
'gateway.paymentIntentId': paymentIntentId,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
if (!transaction) {
|
|
72
|
-
// Try finding by transaction ID directly
|
|
73
|
-
transaction = await TransactionModel.findById(paymentIntentId);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!transaction) {
|
|
77
|
-
throw new TransactionNotFoundError(paymentIntentId);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (transaction.status === 'verified' || transaction.status === 'completed') {
|
|
81
|
-
throw new AlreadyVerifiedError(transaction._id);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Get provider for verification
|
|
85
|
-
const gatewayType = transaction.gateway?.type || 'manual';
|
|
86
|
-
const provider = this.providers[gatewayType];
|
|
87
|
-
|
|
88
|
-
if (!provider) {
|
|
89
|
-
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Verify payment with provider
|
|
93
|
-
let paymentResult = null;
|
|
94
|
-
try {
|
|
95
|
-
paymentResult = await provider.verifyPayment(paymentIntentId);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
this.logger.error('Payment verification failed:', error);
|
|
98
|
-
|
|
99
|
-
// Update transaction as failed
|
|
100
|
-
transaction.status = 'failed';
|
|
101
|
-
transaction.failureReason = error.message;
|
|
102
|
-
transaction.metadata = {
|
|
103
|
-
...transaction.metadata,
|
|
104
|
-
verificationError: error.message,
|
|
105
|
-
failedAt: new Date().toISOString(),
|
|
106
|
-
};
|
|
107
|
-
await transaction.save();
|
|
108
|
-
|
|
109
|
-
// Trigger payment.failed hook
|
|
110
|
-
this._triggerHook('payment.failed', {
|
|
111
|
-
transaction,
|
|
112
|
-
error: error.message,
|
|
113
|
-
provider: gatewayType,
|
|
114
|
-
paymentIntentId,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
throw new PaymentVerificationError(paymentIntentId, error.message);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Validate amount and currency match
|
|
121
|
-
if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
|
|
122
|
-
throw new ValidationError(
|
|
123
|
-
`Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
|
|
124
|
-
{ expected: transaction.amount, actual: paymentResult.amount }
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
|
|
129
|
-
throw new ValidationError(
|
|
130
|
-
`Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
|
|
131
|
-
{ expected: transaction.currency, actual: paymentResult.currency }
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Update transaction based on verification result
|
|
136
|
-
transaction.status = paymentResult.status === 'succeeded' ? 'verified' : paymentResult.status;
|
|
137
|
-
transaction.verifiedAt = paymentResult.paidAt || new Date();
|
|
138
|
-
transaction.verifiedBy = verifiedBy;
|
|
139
|
-
transaction.gateway = {
|
|
140
|
-
...transaction.gateway,
|
|
141
|
-
verificationData: paymentResult.metadata,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
await transaction.save();
|
|
145
|
-
|
|
146
|
-
// Trigger hook
|
|
147
|
-
this._triggerHook('payment.verified', {
|
|
148
|
-
transaction,
|
|
149
|
-
paymentResult,
|
|
150
|
-
verifiedBy,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
transaction,
|
|
155
|
-
paymentResult,
|
|
156
|
-
status: transaction.status,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get payment status
|
|
162
|
-
*
|
|
163
|
-
* @param {String} paymentIntentId - Payment intent ID or transaction ID
|
|
164
|
-
* @returns {Promise<Object>} { transaction, status }
|
|
165
|
-
*/
|
|
166
|
-
async getStatus(paymentIntentId) {
|
|
167
|
-
const TransactionModel = this.models.Transaction;
|
|
168
|
-
|
|
169
|
-
// Find transaction
|
|
170
|
-
let transaction = await TransactionModel.findOne({
|
|
171
|
-
'gateway.paymentIntentId': paymentIntentId,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (!transaction) {
|
|
175
|
-
transaction = await TransactionModel.findById(paymentIntentId);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!transaction) {
|
|
179
|
-
throw new TransactionNotFoundError(paymentIntentId);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Get provider
|
|
183
|
-
const gatewayType = transaction.gateway?.type || 'manual';
|
|
184
|
-
const provider = this.providers[gatewayType];
|
|
185
|
-
|
|
186
|
-
if (!provider) {
|
|
187
|
-
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Get status from provider
|
|
191
|
-
let paymentResult = null;
|
|
192
|
-
try {
|
|
193
|
-
paymentResult = await provider.getStatus(paymentIntentId);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
this.logger.warn('Failed to get payment status from provider:', error);
|
|
196
|
-
// Return transaction status as fallback
|
|
197
|
-
return {
|
|
198
|
-
transaction,
|
|
199
|
-
status: transaction.status,
|
|
200
|
-
provider: gatewayType,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
transaction,
|
|
206
|
-
paymentResult,
|
|
207
|
-
status: paymentResult.status,
|
|
208
|
-
provider: gatewayType,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Refund a payment
|
|
214
|
-
*
|
|
215
|
-
* @param {String} paymentId - Payment intent ID or transaction ID
|
|
216
|
-
* @param {Number} amount - Amount to refund (optional, full refund if not provided)
|
|
217
|
-
* @param {Object} options - Refund options
|
|
218
|
-
* @param {String} options.reason - Refund reason
|
|
219
|
-
* @returns {Promise<Object>} { transaction, refundResult }
|
|
220
|
-
*/
|
|
221
|
-
async refund(paymentId, amount = null, options = {}) {
|
|
222
|
-
const { reason = null } = options;
|
|
223
|
-
|
|
224
|
-
const TransactionModel = this.models.Transaction;
|
|
225
|
-
|
|
226
|
-
// Find transaction
|
|
227
|
-
let transaction = await TransactionModel.findOne({
|
|
228
|
-
'gateway.paymentIntentId': paymentId,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
if (!transaction) {
|
|
232
|
-
transaction = await TransactionModel.findById(paymentId);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (!transaction) {
|
|
236
|
-
throw new TransactionNotFoundError(paymentId);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (transaction.status !== 'verified' && transaction.status !== 'completed') {
|
|
240
|
-
throw new RefundError(transaction._id, 'Only verified/completed transactions can be refunded');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Get provider
|
|
244
|
-
const gatewayType = transaction.gateway?.type || 'manual';
|
|
245
|
-
const provider = this.providers[gatewayType];
|
|
246
|
-
|
|
247
|
-
if (!provider) {
|
|
248
|
-
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Check if provider supports refunds
|
|
252
|
-
const capabilities = provider.getCapabilities();
|
|
253
|
-
if (!capabilities.supportsRefunds) {
|
|
254
|
-
throw new RefundNotSupportedError(gatewayType);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Calculate refundable amount
|
|
258
|
-
const refundedSoFar = transaction.refundedAmount || 0;
|
|
259
|
-
const refundableAmount = transaction.amount - refundedSoFar;
|
|
260
|
-
const refundAmount = amount || refundableAmount;
|
|
261
|
-
|
|
262
|
-
// Validate refund amount
|
|
263
|
-
if (refundAmount <= 0) {
|
|
264
|
-
throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (refundAmount > refundableAmount) {
|
|
268
|
-
throw new ValidationError(
|
|
269
|
-
`Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
|
|
270
|
-
{ refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Refund via provider
|
|
275
|
-
let refundResult = null;
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
refundResult = await provider.refund(paymentId, refundAmount, { reason });
|
|
279
|
-
} catch (error) {
|
|
280
|
-
this.logger.error('Refund failed:', error);
|
|
281
|
-
throw new RefundError(paymentId, error.message);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Create separate refund transaction (EXPENSE) for proper accounting
|
|
285
|
-
const refundTransactionType = this.config.transactionTypeMapping?.refund || TRANSACTION_TYPE.EXPENSE;
|
|
286
|
-
|
|
287
|
-
// Reverse commission proportionally for refund
|
|
288
|
-
const refundCommission = transaction.commission
|
|
289
|
-
? reverseCommission(transaction.commission, transaction.amount, refundAmount)
|
|
290
|
-
: null;
|
|
291
|
-
|
|
292
|
-
const refundTransaction = await TransactionModel.create({
|
|
293
|
-
organizationId: transaction.organizationId,
|
|
294
|
-
customerId: transaction.customerId,
|
|
295
|
-
amount: refundAmount,
|
|
296
|
-
currency: transaction.currency,
|
|
297
|
-
category: transaction.category,
|
|
298
|
-
type: refundTransactionType, // EXPENSE - money going out
|
|
299
|
-
method: transaction.method || 'manual',
|
|
300
|
-
status: 'completed',
|
|
301
|
-
gateway: {
|
|
302
|
-
type: transaction.gateway?.type || 'manual',
|
|
303
|
-
paymentIntentId: refundResult.id,
|
|
304
|
-
provider: refundResult.provider,
|
|
305
|
-
},
|
|
306
|
-
paymentDetails: transaction.paymentDetails,
|
|
307
|
-
...(refundCommission && { commission: refundCommission }), // Reversed commission
|
|
308
|
-
// Polymorphic reference (copy from original transaction)
|
|
309
|
-
...(transaction.referenceId && { referenceId: transaction.referenceId }),
|
|
310
|
-
...(transaction.referenceModel && { referenceModel: transaction.referenceModel }),
|
|
311
|
-
metadata: {
|
|
312
|
-
...transaction.metadata,
|
|
313
|
-
isRefund: true,
|
|
314
|
-
originalTransactionId: transaction._id.toString(),
|
|
315
|
-
refundReason: reason,
|
|
316
|
-
refundResult: refundResult.metadata,
|
|
317
|
-
},
|
|
318
|
-
idempotencyKey: `refund_${transaction._id}_${Date.now()}`,
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Update original transaction status
|
|
322
|
-
const isPartialRefund = refundAmount < transaction.amount;
|
|
323
|
-
transaction.status = isPartialRefund ? 'partially_refunded' : 'refunded';
|
|
324
|
-
transaction.refundedAmount = (transaction.refundedAmount || 0) + refundAmount;
|
|
325
|
-
transaction.refundedAt = refundResult.refundedAt || new Date();
|
|
326
|
-
transaction.metadata = {
|
|
327
|
-
...transaction.metadata,
|
|
328
|
-
refundTransactionId: refundTransaction._id.toString(),
|
|
329
|
-
refundReason: reason,
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
await transaction.save();
|
|
333
|
-
|
|
334
|
-
// Trigger hook
|
|
335
|
-
this._triggerHook('payment.refunded', {
|
|
336
|
-
transaction,
|
|
337
|
-
refundTransaction,
|
|
338
|
-
refundResult,
|
|
339
|
-
refundAmount,
|
|
340
|
-
reason,
|
|
341
|
-
isPartialRefund,
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
transaction,
|
|
346
|
-
refundTransaction,
|
|
347
|
-
refundResult,
|
|
348
|
-
status: transaction.status,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Handle webhook from payment provider
|
|
354
|
-
*
|
|
355
|
-
* @param {String} provider - Provider name
|
|
356
|
-
* @param {Object} payload - Webhook payload
|
|
357
|
-
* @param {Object} headers - Request headers
|
|
358
|
-
* @returns {Promise<Object>} { event, transaction }
|
|
359
|
-
*/
|
|
360
|
-
async handleWebhook(providerName, payload, headers = {}) {
|
|
361
|
-
const provider = this.providers[providerName];
|
|
362
|
-
|
|
363
|
-
if (!provider) {
|
|
364
|
-
throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Check if provider supports webhooks
|
|
368
|
-
const capabilities = provider.getCapabilities();
|
|
369
|
-
if (!capabilities.supportsWebhooks) {
|
|
370
|
-
throw new ProviderCapabilityError(providerName, 'webhooks');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Process webhook via provider
|
|
374
|
-
let webhookEvent = null;
|
|
375
|
-
try {
|
|
376
|
-
webhookEvent = await provider.handleWebhook(payload, headers);
|
|
377
|
-
} catch (error) {
|
|
378
|
-
this.logger.error('Webhook processing failed:', error);
|
|
379
|
-
throw new ProviderError(
|
|
380
|
-
`Webhook processing failed for ${providerName}: ${error.message}`,
|
|
381
|
-
'WEBHOOK_PROCESSING_FAILED',
|
|
382
|
-
{ retryable: false }
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Validate webhook event structure
|
|
387
|
-
if (!webhookEvent?.data?.paymentIntentId) {
|
|
388
|
-
throw new ValidationError(
|
|
389
|
-
`Invalid webhook event structure from ${providerName}: missing paymentIntentId`,
|
|
390
|
-
{ provider: providerName, eventType: webhookEvent?.type }
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Find transaction by payment intent ID from webhook
|
|
395
|
-
const TransactionModel = this.models.Transaction;
|
|
396
|
-
const transaction = await TransactionModel.findOne({
|
|
397
|
-
'gateway.paymentIntentId': webhookEvent.data.paymentIntentId,
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
if (!transaction) {
|
|
401
|
-
this.logger.warn('Transaction not found for webhook event', {
|
|
402
|
-
provider: providerName,
|
|
403
|
-
eventId: webhookEvent.id,
|
|
404
|
-
paymentIntentId: webhookEvent.data.paymentIntentId,
|
|
405
|
-
});
|
|
406
|
-
throw new TransactionNotFoundError(webhookEvent.data.paymentIntentId);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Check for duplicate webhook processing (idempotency)
|
|
410
|
-
if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
|
|
411
|
-
this.logger.warn('Webhook already processed', {
|
|
412
|
-
transactionId: transaction._id,
|
|
413
|
-
eventId: webhookEvent.id,
|
|
414
|
-
});
|
|
415
|
-
return {
|
|
416
|
-
event: webhookEvent,
|
|
417
|
-
transaction,
|
|
418
|
-
status: 'already_processed',
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Update transaction based on webhook event
|
|
423
|
-
transaction.webhook = {
|
|
424
|
-
eventId: webhookEvent.id,
|
|
425
|
-
eventType: webhookEvent.type,
|
|
426
|
-
receivedAt: new Date(),
|
|
427
|
-
processedAt: new Date(),
|
|
428
|
-
data: webhookEvent.data,
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// Update status based on webhook type
|
|
432
|
-
if (webhookEvent.type === 'payment.succeeded') {
|
|
433
|
-
transaction.status = 'verified';
|
|
434
|
-
transaction.verifiedAt = webhookEvent.createdAt;
|
|
435
|
-
} else if (webhookEvent.type === 'payment.failed') {
|
|
436
|
-
transaction.status = 'failed';
|
|
437
|
-
} else if (webhookEvent.type === 'refund.succeeded') {
|
|
438
|
-
transaction.status = 'refunded';
|
|
439
|
-
transaction.refundedAt = webhookEvent.createdAt;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
await transaction.save();
|
|
443
|
-
|
|
444
|
-
// Trigger hook
|
|
445
|
-
this._triggerHook(`payment.webhook.${webhookEvent.type}`, {
|
|
446
|
-
event: webhookEvent,
|
|
447
|
-
transaction,
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
return {
|
|
451
|
-
event: webhookEvent,
|
|
452
|
-
transaction,
|
|
453
|
-
status: 'processed',
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* List payments/transactions with filters
|
|
459
|
-
*
|
|
460
|
-
* @param {Object} filters - Query filters
|
|
461
|
-
* @param {Object} options - Query options (limit, skip, sort)
|
|
462
|
-
* @returns {Promise<Array>} Transactions
|
|
463
|
-
*/
|
|
464
|
-
async list(filters = {}, options = {}) {
|
|
465
|
-
const TransactionModel = this.models.Transaction;
|
|
466
|
-
const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
|
|
467
|
-
|
|
468
|
-
const transactions = await TransactionModel
|
|
469
|
-
.find(filters)
|
|
470
|
-
.limit(limit)
|
|
471
|
-
.skip(skip)
|
|
472
|
-
.sort(sort);
|
|
473
|
-
|
|
474
|
-
return transactions;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Get payment/transaction by ID
|
|
479
|
-
*
|
|
480
|
-
* @param {String} transactionId - Transaction ID
|
|
481
|
-
* @returns {Promise<Object>} Transaction
|
|
482
|
-
*/
|
|
483
|
-
async get(transactionId) {
|
|
484
|
-
const TransactionModel = this.models.Transaction;
|
|
485
|
-
const transaction = await TransactionModel.findById(transactionId);
|
|
486
|
-
|
|
487
|
-
if (!transaction) {
|
|
488
|
-
throw new TransactionNotFoundError(transactionId);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return transaction;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Get provider instance
|
|
496
|
-
*
|
|
497
|
-
* @param {String} providerName - Provider name
|
|
498
|
-
* @returns {Object} Provider instance
|
|
499
|
-
*/
|
|
500
|
-
getProvider(providerName) {
|
|
501
|
-
const provider = this.providers[providerName];
|
|
502
|
-
if (!provider) {
|
|
503
|
-
throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
|
|
504
|
-
}
|
|
505
|
-
return provider;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Trigger event hook (fire-and-forget, non-blocking)
|
|
510
|
-
* @private
|
|
511
|
-
*/
|
|
512
|
-
_triggerHook(event, data) {
|
|
513
|
-
triggerHook(this.hooks, event, data, this.logger);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
export default PaymentService;
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transaction Service
|
|
3
|
-
* @classytic/revenue
|
|
4
|
-
*
|
|
5
|
-
* Thin, focused transaction service for core operations
|
|
6
|
-
* Users handle their own analytics, exports, and complex queries
|
|
7
|
-
*
|
|
8
|
-
* Works with ANY model implementation:
|
|
9
|
-
* - Plain Mongoose models
|
|
10
|
-
* - @classytic/mongokit Repository instances
|
|
11
|
-
* - Any other abstraction with compatible interface
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { TransactionNotFoundError } from '../core/errors.js';
|
|
15
|
-
import { triggerHook } from '../utils/hooks.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Transaction Service
|
|
19
|
-
* Focused on core transaction lifecycle operations
|
|
20
|
-
*/
|
|
21
|
-
export class TransactionService {
|
|
22
|
-
constructor(container) {
|
|
23
|
-
this.container = container;
|
|
24
|
-
this.models = container.get('models');
|
|
25
|
-
this.hooks = container.get('hooks');
|
|
26
|
-
this.logger = container.get('logger');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Get transaction by ID
|
|
31
|
-
*
|
|
32
|
-
* @param {String} transactionId - Transaction ID
|
|
33
|
-
* @returns {Promise<Object>} Transaction
|
|
34
|
-
*/
|
|
35
|
-
async get(transactionId) {
|
|
36
|
-
const TransactionModel = this.models.Transaction;
|
|
37
|
-
const transaction = await TransactionModel.findById(transactionId);
|
|
38
|
-
|
|
39
|
-
if (!transaction) {
|
|
40
|
-
throw new TransactionNotFoundError(transactionId);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return transaction;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* List transactions with filters
|
|
48
|
-
*
|
|
49
|
-
* @param {Object} filters - Query filters
|
|
50
|
-
* @param {Object} options - Query options (limit, skip, sort, populate)
|
|
51
|
-
* @returns {Promise<Object>} { transactions, total, page, limit }
|
|
52
|
-
*/
|
|
53
|
-
async list(filters = {}, options = {}) {
|
|
54
|
-
const TransactionModel = this.models.Transaction;
|
|
55
|
-
const {
|
|
56
|
-
limit = 50,
|
|
57
|
-
skip = 0,
|
|
58
|
-
page = null,
|
|
59
|
-
sort = { createdAt: -1 },
|
|
60
|
-
populate = [],
|
|
61
|
-
} = options;
|
|
62
|
-
|
|
63
|
-
// Calculate pagination
|
|
64
|
-
const actualSkip = page ? (page - 1) * limit : skip;
|
|
65
|
-
|
|
66
|
-
// Build query
|
|
67
|
-
let query = TransactionModel.find(filters)
|
|
68
|
-
.limit(limit)
|
|
69
|
-
.skip(actualSkip)
|
|
70
|
-
.sort(sort);
|
|
71
|
-
|
|
72
|
-
// Apply population if supported
|
|
73
|
-
if (populate.length > 0 && typeof query.populate === 'function') {
|
|
74
|
-
populate.forEach(field => {
|
|
75
|
-
query = query.populate(field);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const transactions = await query;
|
|
80
|
-
|
|
81
|
-
// Count documents (works with both Mongoose and Repository)
|
|
82
|
-
const total = await (TransactionModel.countDocuments
|
|
83
|
-
? TransactionModel.countDocuments(filters)
|
|
84
|
-
: TransactionModel.count(filters));
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
transactions,
|
|
88
|
-
total,
|
|
89
|
-
page: page || Math.floor(actualSkip / limit) + 1,
|
|
90
|
-
limit,
|
|
91
|
-
pages: Math.ceil(total / limit),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Update transaction
|
|
98
|
-
*
|
|
99
|
-
* @param {String} transactionId - Transaction ID
|
|
100
|
-
* @param {Object} updates - Fields to update
|
|
101
|
-
* @returns {Promise<Object>} Updated transaction
|
|
102
|
-
*/
|
|
103
|
-
async update(transactionId, updates) {
|
|
104
|
-
const TransactionModel = this.models.Transaction;
|
|
105
|
-
|
|
106
|
-
// Support both Repository pattern and Mongoose
|
|
107
|
-
let transaction;
|
|
108
|
-
if (typeof TransactionModel.update === 'function') {
|
|
109
|
-
// Repository pattern
|
|
110
|
-
transaction = await TransactionModel.update(transactionId, updates);
|
|
111
|
-
} else {
|
|
112
|
-
// Plain Mongoose
|
|
113
|
-
transaction = await TransactionModel.findByIdAndUpdate(
|
|
114
|
-
transactionId,
|
|
115
|
-
{ $set: updates },
|
|
116
|
-
{ new: true }
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!transaction) {
|
|
121
|
-
throw new TransactionNotFoundError(transactionId);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Trigger hook (fire-and-forget, non-blocking)
|
|
125
|
-
this._triggerHook('transaction.updated', {
|
|
126
|
-
transaction,
|
|
127
|
-
updates,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return transaction;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Trigger event hook (fire-and-forget, non-blocking)
|
|
135
|
-
* @private
|
|
136
|
-
*/
|
|
137
|
-
_triggerHook(event, data) {
|
|
138
|
-
triggerHook(this.hooks, event, data, this.logger);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export default TransactionService;
|