chargeback-guard 2.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/LICENSE +21 -0
- package/README.md +311 -0
- package/docs/api.md +278 -0
- package/docs/architecture.md +281 -0
- package/docs/configuration.md +292 -0
- package/docs/getting-started.md +155 -0
- package/examples/advancedConfig.ts +123 -0
- package/examples/basicUsage.ts +98 -0
- package/examples/stripeIntegration.ts +106 -0
- package/package.json +181 -0
- package/src/ai/fraudDetection.ts +261 -0
- package/src/ai/patternRecognition.ts +218 -0
- package/src/analytics/dashboard.ts +195 -0
- package/src/analytics/metrics.ts +175 -0
- package/src/analytics/predictions.ts +135 -0
- package/src/analytics/reports.ts +221 -0
- package/src/api/controllers.ts +339 -0
- package/src/api/middleware.ts +172 -0
- package/src/api/routes.ts +141 -0
- package/src/config.ts +231 -0
- package/src/core/chargebackGuard.ts +616 -0
- package/src/core/eventEmitter.ts +118 -0
- package/src/core/lifecycle.ts +215 -0
- package/src/database/schema.ts +392 -0
- package/src/dispute/analyzer.ts +317 -0
- package/src/dispute/bankIntegration.ts +274 -0
- package/src/dispute/detector.ts +239 -0
- package/src/dispute/responseEngine.ts +440 -0
- package/src/evidence/collector.ts +426 -0
- package/src/evidence/encryption.ts +168 -0
- package/src/evidence/storage.ts +197 -0
- package/src/evidence/validator.ts +184 -0
- package/src/index.ts +43 -0
- package/src/integrations/paypal.ts +258 -0
- package/src/integrations/stripe.ts +280 -0
- package/src/integrations/webhook.ts +332 -0
- package/src/notifications/email.ts +161 -0
- package/src/notifications/inApp.ts +319 -0
- package/src/notifications/sms.ts +58 -0
- package/src/security/auth.ts +153 -0
- package/src/security/rateLimit.ts +77 -0
- package/src/security/validation.ts +166 -0
- package/src/server.ts +122 -0
- package/src/types/index.ts +790 -0
- package/src/utils/formatters.ts +72 -0
- package/src/utils/helpers.ts +193 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/validators.ts +39 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Main Core Engine (Ultra Pro)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { ChargebackEventEmitter } from './eventEmitter';
|
|
6
|
+
import { LifecycleManager } from './lifecycle';
|
|
7
|
+
import { createLogger } from '../utils/logger';
|
|
8
|
+
import config, { defaultConfig } from '../config';
|
|
9
|
+
import {
|
|
10
|
+
ChargebackGuardConfig,
|
|
11
|
+
PaymentData,
|
|
12
|
+
Dispute,
|
|
13
|
+
Evidence,
|
|
14
|
+
DisputeReply,
|
|
15
|
+
DisputeAnalysis,
|
|
16
|
+
ProtectionStats,
|
|
17
|
+
NotificationEvent,
|
|
18
|
+
RiskLevel,
|
|
19
|
+
DisputeStatus,
|
|
20
|
+
PaymentProvider,
|
|
21
|
+
ApiResponse,
|
|
22
|
+
} from '../types';
|
|
23
|
+
|
|
24
|
+
const log = createLogger('ChargebackGuard');
|
|
25
|
+
|
|
26
|
+
// ────────────────────────────────────────────────────────────
|
|
27
|
+
// LAZY MODULE IMPORTS (to avoid circular deps)
|
|
28
|
+
// ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
31
|
+
const getEvidenceCollector = () => require('../evidence/collector').EvidenceCollector;
|
|
32
|
+
const getEvidenceValidator = () => require('../evidence/validator').EvidenceValidator;
|
|
33
|
+
const getEvidenceStorage = () => require('../evidence/storage').EvidenceStorage;
|
|
34
|
+
const getDisputeDetector = () => require('../dispute/detector').DisputeDetector;
|
|
35
|
+
const getDisputeAnalyzer = () => require('../dispute/analyzer').DisputeAnalyzer;
|
|
36
|
+
const getResponseEngine = () => require('../dispute/responseEngine').ResponseEngine;
|
|
37
|
+
const getFraudDetection = () => require('../ai/fraudDetection').FraudDetection;
|
|
38
|
+
const getDatabaseManager = () => require('../database/schema').DatabaseManager;
|
|
39
|
+
const getNotificationSvc = () => require('../notifications/inApp').NotificationService;
|
|
40
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
41
|
+
|
|
42
|
+
// ────────────────────────────────────────────────────────────
|
|
43
|
+
// REGISTRATION RESULT
|
|
44
|
+
// ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export interface RegistrationResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
orderId: string;
|
|
49
|
+
protectionActive: boolean;
|
|
50
|
+
trustScore: number;
|
|
51
|
+
riskLevel: RiskLevel;
|
|
52
|
+
evidenceCollectionId?: string;
|
|
53
|
+
warnings?: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DisputeHandlingResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
disputeId: string;
|
|
59
|
+
orderId?: string;
|
|
60
|
+
actionTaken: 'auto-reply_submitted' | 'manual_review_required' | 'accepted' | 'skipped';
|
|
61
|
+
confidence?: number;
|
|
62
|
+
riskLevel?: RiskLevel;
|
|
63
|
+
replySubmitted: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────
|
|
67
|
+
// MAIN CLASS
|
|
68
|
+
// ────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export class ChargebackGuard {
|
|
71
|
+
public readonly events: ChargebackEventEmitter;
|
|
72
|
+
public readonly lifecycle: LifecycleManager;
|
|
73
|
+
|
|
74
|
+
private readonly cfg: ChargebackGuardConfig;
|
|
75
|
+
private evidenceCollector: unknown;
|
|
76
|
+
private evidenceValidator: unknown;
|
|
77
|
+
private evidenceStorage: unknown;
|
|
78
|
+
private disputeDetector: unknown;
|
|
79
|
+
private disputeAnalyzer: unknown;
|
|
80
|
+
private responseEngine: unknown;
|
|
81
|
+
private fraudDetection: unknown;
|
|
82
|
+
private db: unknown;
|
|
83
|
+
private notificationSvc: unknown;
|
|
84
|
+
|
|
85
|
+
constructor(options: Partial<ChargebackGuardConfig> = {}) {
|
|
86
|
+
this.cfg = { ...defaultConfig, ...options } as ChargebackGuardConfig;
|
|
87
|
+
this.events = new ChargebackEventEmitter();
|
|
88
|
+
this.lifecycle = new LifecycleManager();
|
|
89
|
+
|
|
90
|
+
this._registerLifecycleHooks();
|
|
91
|
+
this._registerDefaultEventHandlers();
|
|
92
|
+
|
|
93
|
+
log.info('ChargebackGuard instance created', {
|
|
94
|
+
environment: this.cfg.environment,
|
|
95
|
+
autoReply: this.cfg.autoReply,
|
|
96
|
+
evidenceCollection: this.cfg.evidenceCollection,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ──────────────────────────────────────────
|
|
101
|
+
// INITIALIZATION
|
|
102
|
+
// ──────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
async initialize(): Promise<void> {
|
|
105
|
+
await this.lifecycle.initialize();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async shutdown(): Promise<void> {
|
|
109
|
+
await this.lifecycle.shutdown();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ──────────────────────────────────────────
|
|
113
|
+
// CORE: REGISTER PAYMENT
|
|
114
|
+
// ──────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Register a new payment for chargeback protection.
|
|
118
|
+
* Call this immediately after a successful charge.
|
|
119
|
+
*/
|
|
120
|
+
async registerPayment(paymentData: PaymentData): Promise<RegistrationResult> {
|
|
121
|
+
const operationStart = Date.now();
|
|
122
|
+
log.info(`Registering payment: ${paymentData.orderId}`, {
|
|
123
|
+
amount: paymentData.amount,
|
|
124
|
+
currency: paymentData.currency,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// ── 1. Validate input ──────────────────
|
|
129
|
+
this._validatePaymentData(paymentData);
|
|
130
|
+
|
|
131
|
+
// ── 2. Run fraud pre-check ─────────────
|
|
132
|
+
const fraudResult = await this._runFraudPreCheck(paymentData);
|
|
133
|
+
if (fraudResult.isFraud && fraudResult.probability > 0.95) {
|
|
134
|
+
this.events.emitEvent(NotificationEvent.FRAUD_DETECTED, {
|
|
135
|
+
orderId: paymentData.orderId,
|
|
136
|
+
probability: fraudResult.probability,
|
|
137
|
+
});
|
|
138
|
+
log.warn(`Potential fraud detected for order ${paymentData.orderId}`, {
|
|
139
|
+
probability: fraudResult.probability,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 3. Store the order ─────────────────
|
|
144
|
+
const db = this._getDb();
|
|
145
|
+
await db.orders.create({
|
|
146
|
+
orderId: paymentData.orderId,
|
|
147
|
+
amount: paymentData.amount,
|
|
148
|
+
currency: paymentData.currency,
|
|
149
|
+
customerEmail: paymentData.customerEmail,
|
|
150
|
+
provider: paymentData.provider ?? PaymentProvider.STRIPE,
|
|
151
|
+
status: 'active',
|
|
152
|
+
registeredAt: new Date().toISOString(),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── 4. Collect evidence ────────────────
|
|
156
|
+
let evidence: Evidence | undefined;
|
|
157
|
+
let evidenceCollectionId: string | undefined;
|
|
158
|
+
|
|
159
|
+
if (this.cfg.evidenceCollection !== false) {
|
|
160
|
+
const collector = this._getEvidenceCollector();
|
|
161
|
+
const validator = this._getEvidenceValidator();
|
|
162
|
+
const storage = this._getEvidenceStorage();
|
|
163
|
+
|
|
164
|
+
const rawEvidence = await collector.collect({
|
|
165
|
+
orderId: paymentData.orderId,
|
|
166
|
+
customerIp: paymentData.customerIp,
|
|
167
|
+
userAgent: paymentData.userAgent,
|
|
168
|
+
sessionData: paymentData.sessionData,
|
|
169
|
+
deviceFingerprint: paymentData.deviceFingerprint,
|
|
170
|
+
amount: paymentData.amount,
|
|
171
|
+
currency: paymentData.currency,
|
|
172
|
+
transactionId: paymentData.transactionId,
|
|
173
|
+
shippingAddress: paymentData.shippingAddress,
|
|
174
|
+
customerEmail: paymentData.customerEmail,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
evidence = await validator.validate(rawEvidence);
|
|
178
|
+
evidenceCollectionId = await storage.store(evidence);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 5. Emit success event ──────────────
|
|
182
|
+
const trustScore = evidence?.trustScore ?? 75;
|
|
183
|
+
const riskLevel = this._scoreToRiskLevel(trustScore);
|
|
184
|
+
|
|
185
|
+
this.events.emitEvent(NotificationEvent.PAYMENT_REGISTERED, {
|
|
186
|
+
orderId: paymentData.orderId,
|
|
187
|
+
trustScore,
|
|
188
|
+
riskLevel,
|
|
189
|
+
durationMs: Date.now() - operationStart,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (riskLevel === RiskLevel.HIGH || riskLevel === RiskLevel.CRITICAL) {
|
|
193
|
+
this.events.emitEvent(NotificationEvent.HIGH_RISK_TRANSACTION, {
|
|
194
|
+
orderId: paymentData.orderId,
|
|
195
|
+
riskLevel,
|
|
196
|
+
fraudProbability: fraudResult?.probability ?? 0,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result: RegistrationResult = {
|
|
201
|
+
success: true,
|
|
202
|
+
orderId: paymentData.orderId,
|
|
203
|
+
protectionActive: true,
|
|
204
|
+
trustScore,
|
|
205
|
+
riskLevel,
|
|
206
|
+
evidenceCollectionId,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
log.info(`Payment registered successfully: ${paymentData.orderId}`, {
|
|
210
|
+
trustScore,
|
|
211
|
+
riskLevel,
|
|
212
|
+
durationMs: Date.now() - operationStart,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return result;
|
|
216
|
+
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
+
log.error(`Payment registration failed: ${paymentData.orderId}`, { error: message });
|
|
220
|
+
this.events.emitEvent(NotificationEvent.ERROR, {
|
|
221
|
+
type: 'payment_registration_failed',
|
|
222
|
+
orderId: paymentData.orderId,
|
|
223
|
+
error: message,
|
|
224
|
+
});
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ──────────────────────────────────────────
|
|
230
|
+
// CORE: HANDLE DISPUTE
|
|
231
|
+
// ──────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handle an incoming dispute/chargeback notification from the bank.
|
|
235
|
+
* This is the heart of the auto-reply engine.
|
|
236
|
+
*/
|
|
237
|
+
async handleDispute(dispute: Dispute): Promise<DisputeHandlingResult> {
|
|
238
|
+
const operationStart = Date.now();
|
|
239
|
+
log.info(`Handling dispute: ${dispute.id}`, {
|
|
240
|
+
reason: dispute.reason,
|
|
241
|
+
amount: dispute.amount,
|
|
242
|
+
orderId: dispute.orderId,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.events.emitEvent(NotificationEvent.DISPUTE_DETECTED, {
|
|
246
|
+
disputeId: dispute.id,
|
|
247
|
+
reason: dispute.reason,
|
|
248
|
+
amount: dispute.amount,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// ── 1. Analyze the dispute ─────────────
|
|
253
|
+
const analyzer = this._getDisputeAnalyzer();
|
|
254
|
+
const analysis: DisputeAnalysis = await analyzer.analyze(dispute);
|
|
255
|
+
|
|
256
|
+
log.info(`Dispute analysis complete: ${dispute.id}`, {
|
|
257
|
+
riskLevel: analysis.riskLevel,
|
|
258
|
+
confidenceScore: analysis.confidenceScore,
|
|
259
|
+
recommendedAction: analysis.recommendedAction,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ── 2. Load stored evidence ────────────
|
|
263
|
+
const storage = this._getEvidenceStorage();
|
|
264
|
+
const evidence: Evidence | null = dispute.orderId
|
|
265
|
+
? await storage.findByOrderId(dispute.orderId)
|
|
266
|
+
: null;
|
|
267
|
+
|
|
268
|
+
if (!evidence) {
|
|
269
|
+
log.warn(`No evidence found for order: ${dispute.orderId}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── 3. Decide action ───────────────────
|
|
273
|
+
if (analysis.recommendedAction === 'accept') {
|
|
274
|
+
log.info(`Accepting dispute (low confidence): ${dispute.id}`);
|
|
275
|
+
await this._updateDisputeRecord(dispute.id, dispute.orderId, DisputeStatus.CHARGE_REFUNDED);
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
disputeId: dispute.id,
|
|
279
|
+
orderId: dispute.orderId,
|
|
280
|
+
actionTaken: 'accepted',
|
|
281
|
+
confidence: analysis.confidenceScore,
|
|
282
|
+
riskLevel: analysis.riskLevel,
|
|
283
|
+
replySubmitted: false,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── 4. Generate reply ──────────────────
|
|
288
|
+
const engine = this._getResponseEngine();
|
|
289
|
+
const reply: DisputeReply = await engine.generateReply({
|
|
290
|
+
dispute,
|
|
291
|
+
evidence,
|
|
292
|
+
analysis,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── 5. Submit to bank (auto-reply) ─────
|
|
296
|
+
let replySubmitted = false;
|
|
297
|
+
if (this.cfg.autoReply !== false) {
|
|
298
|
+
try {
|
|
299
|
+
await this._submitReplyToProvider(dispute, reply);
|
|
300
|
+
replySubmitted = true;
|
|
301
|
+
log.info(`Reply submitted to provider: ${dispute.id}`);
|
|
302
|
+
} catch (submitErr) {
|
|
303
|
+
const msg = submitErr instanceof Error ? submitErr.message : String(submitErr);
|
|
304
|
+
log.error(`Failed to submit reply: ${dispute.id}`, { error: msg });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── 6. Persist dispute record ──────────
|
|
309
|
+
await this._updateDisputeRecord(
|
|
310
|
+
dispute.id,
|
|
311
|
+
dispute.orderId,
|
|
312
|
+
replySubmitted ? DisputeStatus.UNDER_REVIEW : DisputeStatus.NEEDS_RESPONSE,
|
|
313
|
+
reply
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// ── 7. Emit events ─────────────────────
|
|
317
|
+
this.events.emitEvent(NotificationEvent.DISPUTE_REPLIED, {
|
|
318
|
+
disputeId: dispute.id,
|
|
319
|
+
orderId: dispute.orderId,
|
|
320
|
+
replySubmitted,
|
|
321
|
+
confidenceScore: analysis.confidenceScore,
|
|
322
|
+
durationMs: Date.now() - operationStart,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
success: true,
|
|
327
|
+
disputeId: dispute.id,
|
|
328
|
+
orderId: dispute.orderId,
|
|
329
|
+
actionTaken: replySubmitted ? 'auto-reply_submitted' : 'manual_review_required',
|
|
330
|
+
confidence: analysis.confidenceScore,
|
|
331
|
+
riskLevel: analysis.riskLevel,
|
|
332
|
+
replySubmitted,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
337
|
+
log.error(`Dispute handling failed: ${dispute.id}`, { error: message });
|
|
338
|
+
this.events.emitEvent(NotificationEvent.ERROR, {
|
|
339
|
+
type: 'dispute_handling_failed',
|
|
340
|
+
disputeId: dispute.id,
|
|
341
|
+
error: message,
|
|
342
|
+
});
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ──────────────────────────────────────────
|
|
348
|
+
// STATS & ANALYTICS
|
|
349
|
+
// ──────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
async getProtectionStats(fromDate?: Date, toDate?: Date): Promise<ProtectionStats> {
|
|
352
|
+
const db = this._getDb();
|
|
353
|
+
|
|
354
|
+
const [totalOrders, totalDisputes, wonDisputes, lostDisputes, pendingDisputes] =
|
|
355
|
+
await Promise.all([
|
|
356
|
+
db.orders.count(fromDate, toDate),
|
|
357
|
+
db.disputes.count(fromDate, toDate),
|
|
358
|
+
db.disputes.countByStatus(DisputeStatus.WON, fromDate, toDate),
|
|
359
|
+
db.disputes.countByStatus(DisputeStatus.LOST, fromDate, toDate),
|
|
360
|
+
db.disputes.countByStatus(DisputeStatus.NEEDS_RESPONSE, fromDate, toDate),
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
const resolvedDisputes = wonDisputes + lostDisputes;
|
|
364
|
+
const winRate = totalDisputes > 0 ? (wonDisputes / totalDisputes) * 100 : 0;
|
|
365
|
+
const chargebackRate = totalOrders > 0 ? (totalDisputes / totalOrders) * 100 : 0;
|
|
366
|
+
|
|
367
|
+
const totalMoneyRecovered = await db.disputes.sumAmountByStatus(DisputeStatus.WON, fromDate, toDate);
|
|
368
|
+
const totalMoneyLost = await db.disputes.sumAmountByStatus(DisputeStatus.LOST, fromDate, toDate);
|
|
369
|
+
const estimatedSavings = totalMoneyRecovered * 1.25; // including avoided fees
|
|
370
|
+
|
|
371
|
+
const avgResolutionTime = await db.disputes.avgResolutionTime(fromDate, toDate);
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
totalOrders,
|
|
375
|
+
totalDisputes,
|
|
376
|
+
resolvedDisputes,
|
|
377
|
+
wonDisputes,
|
|
378
|
+
lostDisputes,
|
|
379
|
+
pendingDisputes,
|
|
380
|
+
winRate: parseFloat(winRate.toFixed(2)),
|
|
381
|
+
totalMoneyRecovered,
|
|
382
|
+
totalMoneyLost,
|
|
383
|
+
averageResolutionTime: avgResolutionTime,
|
|
384
|
+
chargebackRate: parseFloat(chargebackRate.toFixed(4)),
|
|
385
|
+
estimatedSavings,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async getHealth(): Promise<ApiResponse<unknown>> {
|
|
390
|
+
const health = await this.lifecycle.getSystemHealth();
|
|
391
|
+
return {
|
|
392
|
+
success: health.status !== 'unhealthy',
|
|
393
|
+
data: health,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ──────────────────────────────────────────
|
|
398
|
+
// PRIVATE HELPERS
|
|
399
|
+
// ──────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
private _validatePaymentData(data: PaymentData): void {
|
|
402
|
+
if (!data.orderId?.trim()) { throw new Error('orderId is required'); }
|
|
403
|
+
if (!data.amount || data.amount <= 0) { throw new Error('amount must be positive'); }
|
|
404
|
+
if (!data.currency?.trim()) { throw new Error('currency is required'); }
|
|
405
|
+
if (!data.customerEmail?.trim()) { throw new Error('customerEmail is required'); }
|
|
406
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.customerEmail)) {
|
|
407
|
+
throw new Error('customerEmail is invalid');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private async _runFraudPreCheck(data: PaymentData): Promise<{
|
|
412
|
+
isFraud: boolean;
|
|
413
|
+
probability: number;
|
|
414
|
+
}> {
|
|
415
|
+
try {
|
|
416
|
+
const fraud = this._getFraudDetection();
|
|
417
|
+
return await fraud.quickCheck(data);
|
|
418
|
+
} catch {
|
|
419
|
+
return { isFraud: false, probability: 0 };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async _submitReplyToProvider(dispute: Dispute, reply: DisputeReply): Promise<void> {
|
|
424
|
+
// Dynamic import to avoid circular deps at module load time
|
|
425
|
+
if (dispute.provider === PaymentProvider.STRIPE) {
|
|
426
|
+
const { StripeIntegration } = require('../integrations/stripe');
|
|
427
|
+
const stripe = new StripeIntegration(config.stripe.secretKey);
|
|
428
|
+
await stripe.submitDisputeEvidence(dispute.id, reply);
|
|
429
|
+
} else if (dispute.provider === PaymentProvider.PAYPAL) {
|
|
430
|
+
const { PayPalIntegration } = require('../integrations/paypal');
|
|
431
|
+
const paypal = new PayPalIntegration(
|
|
432
|
+
config.paypal.clientId,
|
|
433
|
+
config.paypal.clientSecret,
|
|
434
|
+
config.paypal.mode
|
|
435
|
+
);
|
|
436
|
+
await paypal.submitDisputeEvidence(dispute.id, reply);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private async _updateDisputeRecord(
|
|
441
|
+
disputeId: string,
|
|
442
|
+
orderId: string | undefined,
|
|
443
|
+
status: DisputeStatus,
|
|
444
|
+
reply?: DisputeReply
|
|
445
|
+
): Promise<void> {
|
|
446
|
+
try {
|
|
447
|
+
const db = this._getDb();
|
|
448
|
+
await db.disputes.upsert({
|
|
449
|
+
disputeId,
|
|
450
|
+
orderId: orderId ?? '',
|
|
451
|
+
status,
|
|
452
|
+
replyJson: reply ? JSON.stringify(reply) : undefined,
|
|
453
|
+
replySubmittedAt: reply ? new Date().toISOString() : undefined,
|
|
454
|
+
});
|
|
455
|
+
} catch (err) {
|
|
456
|
+
log.warn('Failed to update dispute record', {
|
|
457
|
+
disputeId,
|
|
458
|
+
error: err instanceof Error ? err.message : String(err),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private _scoreToRiskLevel(trustScore: number): RiskLevel {
|
|
464
|
+
if (trustScore >= 90) { return RiskLevel.VERY_LOW; }
|
|
465
|
+
if (trustScore >= 75) { return RiskLevel.LOW; }
|
|
466
|
+
if (trustScore >= 55) { return RiskLevel.MEDIUM; }
|
|
467
|
+
if (trustScore >= 35) { return RiskLevel.HIGH; }
|
|
468
|
+
return RiskLevel.CRITICAL;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ──────────────────────────────────────────
|
|
472
|
+
// LIFECYCLE HOOKS
|
|
473
|
+
// ──────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
private _registerLifecycleHooks(): void {
|
|
476
|
+
this.lifecycle.registerInitHook({
|
|
477
|
+
name: 'database',
|
|
478
|
+
fn: async () => {
|
|
479
|
+
const db = this._getDb();
|
|
480
|
+
await db.initialize();
|
|
481
|
+
},
|
|
482
|
+
timeout: 15000,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
this.lifecycle.registerInitHook({
|
|
486
|
+
name: 'fraud-model',
|
|
487
|
+
fn: async () => {
|
|
488
|
+
const fraud = this._getFraudDetection();
|
|
489
|
+
await fraud.loadModel();
|
|
490
|
+
},
|
|
491
|
+
timeout: 30000,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
this.lifecycle.registerShutdownHook({
|
|
495
|
+
name: 'database',
|
|
496
|
+
fn: async () => {
|
|
497
|
+
const db = this._getDb();
|
|
498
|
+
await db.destroy();
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Register health checkers
|
|
503
|
+
this.lifecycle.registerHealthChecker('database', async () => {
|
|
504
|
+
try {
|
|
505
|
+
const db = this._getDb();
|
|
506
|
+
await db.healthCheck();
|
|
507
|
+
return {
|
|
508
|
+
name: 'database',
|
|
509
|
+
status: 'healthy' as const,
|
|
510
|
+
checkedAt: new Date().toISOString(),
|
|
511
|
+
};
|
|
512
|
+
} catch (err) {
|
|
513
|
+
return {
|
|
514
|
+
name: 'database',
|
|
515
|
+
status: 'unhealthy' as const,
|
|
516
|
+
message: err instanceof Error ? err.message : String(err),
|
|
517
|
+
checkedAt: new Date().toISOString(),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ──────────────────────────────────────────
|
|
524
|
+
// DEFAULT EVENT HANDLERS
|
|
525
|
+
// ──────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
private _registerDefaultEventHandlers(): void {
|
|
528
|
+
this.events.on(NotificationEvent.PAYMENT_REGISTERED, ({ data }) => {
|
|
529
|
+
log.info(`✅ Payment registered: ${data['orderId']} | Trust: ${data['trustScore']} | Risk: ${data['riskLevel']}`);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
this.events.on(NotificationEvent.DISPUTE_DETECTED, ({ data }) => {
|
|
533
|
+
log.warn(`🚨 Dispute detected: ${data['disputeId']} | Reason: ${data['reason']} | Amount: $${data['amount']}`);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
this.events.on(NotificationEvent.DISPUTE_REPLIED, ({ data }) => {
|
|
537
|
+
log.info(`📤 Reply sent for: ${data['disputeId']} | Confidence: ${data['confidenceScore']}`);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
this.events.on(NotificationEvent.DISPUTE_WON, ({ data }) => {
|
|
541
|
+
log.info(`🏆 Dispute WON: ${data['disputeId']} | Recovered: $${data['amount']}`);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
this.events.on(NotificationEvent.DISPUTE_LOST, ({ data }) => {
|
|
545
|
+
log.warn(`❌ Dispute LOST: ${data['disputeId']} | Amount: $${data['amount']}`);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
this.events.on(NotificationEvent.FRAUD_DETECTED, ({ data }) => {
|
|
549
|
+
log.warn(`🔴 FRAUD ALERT: Order ${data['orderId']} | Probability: ${data['probability']}`);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
this.events.on(NotificationEvent.ERROR, ({ data }) => {
|
|
553
|
+
log.error(`❌ Error: ${data['type']}`, { error: data['error'] });
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ──────────────────────────────────────────
|
|
558
|
+
// LAZY SERVICE GETTERS
|
|
559
|
+
// ──────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
private _getEvidenceCollector(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
562
|
+
if (!this.evidenceCollector) {
|
|
563
|
+
const Cls = getEvidenceCollector();
|
|
564
|
+
this.evidenceCollector = new Cls();
|
|
565
|
+
}
|
|
566
|
+
return this.evidenceCollector;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private _getEvidenceValidator(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
570
|
+
if (!this.evidenceValidator) {
|
|
571
|
+
const Cls = getEvidenceValidator();
|
|
572
|
+
this.evidenceValidator = new Cls();
|
|
573
|
+
}
|
|
574
|
+
return this.evidenceValidator;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private _getEvidenceStorage(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
578
|
+
if (!this.evidenceStorage) {
|
|
579
|
+
const Cls = getEvidenceStorage();
|
|
580
|
+
this.evidenceStorage = new Cls();
|
|
581
|
+
}
|
|
582
|
+
return this.evidenceStorage;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private _getDisputeAnalyzer(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
586
|
+
if (!this.disputeAnalyzer) {
|
|
587
|
+
const Cls = getDisputeAnalyzer();
|
|
588
|
+
this.disputeAnalyzer = new Cls();
|
|
589
|
+
}
|
|
590
|
+
return this.disputeAnalyzer;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private _getResponseEngine(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
594
|
+
if (!this.responseEngine) {
|
|
595
|
+
const Cls = getResponseEngine();
|
|
596
|
+
this.responseEngine = new Cls();
|
|
597
|
+
}
|
|
598
|
+
return this.responseEngine;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private _getFraudDetection(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
602
|
+
if (!this.fraudDetection) {
|
|
603
|
+
const Cls = getFraudDetection();
|
|
604
|
+
this.fraudDetection = new Cls(config.ai);
|
|
605
|
+
}
|
|
606
|
+
return this.fraudDetection;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private _getDb(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
610
|
+
if (!this.db) {
|
|
611
|
+
const Cls = getDatabaseManager();
|
|
612
|
+
this.db = new Cls(config.db);
|
|
613
|
+
}
|
|
614
|
+
return this.db;
|
|
615
|
+
}
|
|
616
|
+
}
|