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,317 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Dispute Analyzer
|
|
3
|
+
// Scores a dispute and recommends action
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
import { createLogger } from '../utils/logger';
|
|
7
|
+
import {
|
|
8
|
+
Dispute,
|
|
9
|
+
DisputeAnalysis,
|
|
10
|
+
Evidence,
|
|
11
|
+
DisputeReason,
|
|
12
|
+
RiskLevel,
|
|
13
|
+
ConfidenceLevel,
|
|
14
|
+
} from '../types';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('DisputeAnalyzer');
|
|
17
|
+
|
|
18
|
+
// ────────────────────────────────────────────────────────────
|
|
19
|
+
// ANALYSIS WEIGHTS
|
|
20
|
+
// ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const WEIGHTS = {
|
|
23
|
+
deliveryProof: 0.30,
|
|
24
|
+
deviceConsistency: 0.20,
|
|
25
|
+
customerReputation: 0.20,
|
|
26
|
+
transactionNormality: 0.15,
|
|
27
|
+
evidenceCompleteness: 0.15,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ────────────────────────────────────────────────────────────
|
|
31
|
+
// DISPUTE ANALYZER
|
|
32
|
+
// ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export class DisputeAnalyzer {
|
|
35
|
+
|
|
36
|
+
async analyze(dispute: Dispute, evidence?: Evidence): Promise<DisputeAnalysis> {
|
|
37
|
+
log.debug(`Analyzing dispute: ${dispute.id}`);
|
|
38
|
+
|
|
39
|
+
const deliveryProof = this._scoreDeliveryProof(dispute, evidence);
|
|
40
|
+
const deviceConsistency = this._scoreDeviceConsistency(evidence);
|
|
41
|
+
const customerReputation = this._scoreCustomerReputation(evidence);
|
|
42
|
+
const transactionNormality = this._scoreTransactionNormality(dispute, evidence);
|
|
43
|
+
const evidenceCompleteness = this._scoreEvidenceCompleteness(evidence);
|
|
44
|
+
|
|
45
|
+
const confidenceScore =
|
|
46
|
+
deliveryProof * WEIGHTS.deliveryProof +
|
|
47
|
+
deviceConsistency * WEIGHTS.deviceConsistency +
|
|
48
|
+
customerReputation * WEIGHTS.customerReputation +
|
|
49
|
+
transactionNormality * WEIGHTS.transactionNormality +
|
|
50
|
+
evidenceCompleteness * WEIGHTS.evidenceCompleteness;
|
|
51
|
+
|
|
52
|
+
const riskLevel = this._confidenceToRisk(confidenceScore);
|
|
53
|
+
const fraudProbability = this._estimateFraudProbability(dispute, evidence);
|
|
54
|
+
const recommendedAction = this._recommendAction(confidenceScore, fraudProbability, dispute);
|
|
55
|
+
const analysisReasons = this._generateReasons(
|
|
56
|
+
dispute,
|
|
57
|
+
evidence,
|
|
58
|
+
{ deliveryProof, deviceConsistency, customerReputation, transactionNormality }
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const analysis: DisputeAnalysis = {
|
|
62
|
+
disputeId: dispute.id,
|
|
63
|
+
riskLevel,
|
|
64
|
+
confidenceScore: parseFloat(confidenceScore.toFixed(4)),
|
|
65
|
+
deliveryProofStrength: deliveryProof,
|
|
66
|
+
deviceConsistency,
|
|
67
|
+
customerReputationScore: customerReputation,
|
|
68
|
+
transactionNormality,
|
|
69
|
+
fraudProbability,
|
|
70
|
+
recommendedAction,
|
|
71
|
+
analysisReasons,
|
|
72
|
+
analyzedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
log.info(`Dispute analysis complete: ${dispute.id}`, {
|
|
76
|
+
confidenceScore: analysis.confidenceScore,
|
|
77
|
+
riskLevel,
|
|
78
|
+
recommendation: recommendedAction,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return analysis;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ──────────────────────────────────────────
|
|
85
|
+
// SCORING: Delivery Proof
|
|
86
|
+
// ──────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
private _scoreDeliveryProof(dispute: Dispute, evidence?: Evidence): number {
|
|
89
|
+
if (dispute.reason !== DisputeReason.PRODUCT_NOT_RECEIVED) {
|
|
90
|
+
return 0.85; // delivery proof not the core issue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let score = 0.2; // baseline
|
|
94
|
+
|
|
95
|
+
const shipping = evidence?.data?.shippingData;
|
|
96
|
+
if (!shipping) { return score; }
|
|
97
|
+
|
|
98
|
+
if (shipping.trackingNumber) { score += 0.35; }
|
|
99
|
+
if (shipping.deliveryDate) { score += 0.20; }
|
|
100
|
+
if (shipping.signature) { score += 0.15; }
|
|
101
|
+
if (shipping.gpsCoordinates) { score += 0.10; }
|
|
102
|
+
|
|
103
|
+
return Math.min(1.0, score);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ──────────────────────────────────────────
|
|
107
|
+
// SCORING: Device Consistency
|
|
108
|
+
// ──────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
private _scoreDeviceConsistency(evidence?: Evidence): number {
|
|
111
|
+
if (!evidence?.data) { return 0.3; }
|
|
112
|
+
|
|
113
|
+
let score = 0.3; // baseline
|
|
114
|
+
|
|
115
|
+
const cd = evidence.data.customerData;
|
|
116
|
+
if (cd?.ipAddress && cd.ipAddress !== '0.0.0.0') { score += 0.15; }
|
|
117
|
+
if (cd?.userAgent && cd.userAgent !== 'Unknown') { score += 0.10; }
|
|
118
|
+
if (cd?.deviceId) { score += 0.15; }
|
|
119
|
+
|
|
120
|
+
const fp = evidence.data.deviceFingerprint;
|
|
121
|
+
if (fp?.fingerprint) { score += 0.15; }
|
|
122
|
+
if (fp?.screenResolution) { score += 0.05; }
|
|
123
|
+
if (fp?.timezone) { score += 0.05; }
|
|
124
|
+
if (fp?.language) { score += 0.05; }
|
|
125
|
+
|
|
126
|
+
return Math.min(1.0, score);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ──────────────────────────────────────────
|
|
130
|
+
// SCORING: Customer Reputation
|
|
131
|
+
// ──────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
private _scoreCustomerReputation(evidence?: Evidence): number {
|
|
134
|
+
const history = evidence?.data?.customerHistory;
|
|
135
|
+
if (!history) { return 0.5; }
|
|
136
|
+
|
|
137
|
+
let score = 0.4; // baseline
|
|
138
|
+
|
|
139
|
+
if (history.accountAge > 365) { score += 0.20; }
|
|
140
|
+
else if (history.accountAge > 90) { score += 0.10; }
|
|
141
|
+
else if (history.accountAge > 30) { score += 0.05; }
|
|
142
|
+
|
|
143
|
+
if (history.previousPurchases >= 10) { score += 0.20; }
|
|
144
|
+
else if (history.previousPurchases >= 3) { score += 0.10; }
|
|
145
|
+
|
|
146
|
+
if (history.previousDisputes === 0) { score += 0.20; }
|
|
147
|
+
else if (history.previousDisputes === 1) { score -= 0.10; }
|
|
148
|
+
else { score -= 0.30; }
|
|
149
|
+
|
|
150
|
+
if (history.trustScore > 0.8) { score += 0.10; }
|
|
151
|
+
|
|
152
|
+
return Math.max(0, Math.min(1.0, score));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ──────────────────────────────────────────
|
|
156
|
+
// SCORING: Transaction Normality
|
|
157
|
+
// ──────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
private _scoreTransactionNormality(dispute: Dispute, evidence?: Evidence): number {
|
|
160
|
+
let score = 0.5; // baseline
|
|
161
|
+
|
|
162
|
+
const interaction = evidence?.data?.interactionData;
|
|
163
|
+
if (interaction) {
|
|
164
|
+
if (interaction.sessionDuration > 120) { score += 0.15; }
|
|
165
|
+
if (interaction.pageViews?.length > 3) { score += 0.10; }
|
|
166
|
+
if (interaction.timeOnCheckout > 30) { score += 0.10; }
|
|
167
|
+
if (interaction.formInteractions?.length > 0) { score += 0.05; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const paymentLog = evidence?.data?.paymentLog;
|
|
171
|
+
if (paymentLog) {
|
|
172
|
+
if (paymentLog.attempts === 1) { score += 0.10; }
|
|
173
|
+
else if (paymentLog.failed > 3) { score -= 0.20; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Unusual amount warning
|
|
177
|
+
if (dispute.amount > 100000) { score -= 0.10; } // > $1000
|
|
178
|
+
|
|
179
|
+
// IP reputation
|
|
180
|
+
const ipRep = evidence?.data?.customerData?.ipReputation;
|
|
181
|
+
if (ipRep !== undefined) {
|
|
182
|
+
if (ipRep < 0.3) { score -= 0.30; }
|
|
183
|
+
else if (ipRep < 0.5) { score -= 0.15; }
|
|
184
|
+
else if (ipRep > 0.8) { score += 0.10; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Math.max(0, Math.min(1.0, score));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ──────────────────────────────────────────
|
|
191
|
+
// SCORING: Evidence Completeness
|
|
192
|
+
// ──────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
private _scoreEvidenceCompleteness(evidence?: Evidence): number {
|
|
195
|
+
if (!evidence) { return 0.1; }
|
|
196
|
+
|
|
197
|
+
const checks = [
|
|
198
|
+
!!evidence.data.customerData?.ipAddress,
|
|
199
|
+
!!evidence.data.customerData?.userAgent,
|
|
200
|
+
!!evidence.data.customerData?.deviceId,
|
|
201
|
+
!!evidence.data.transactionData?.timestamp,
|
|
202
|
+
!!evidence.data.transactionData?.transactionId,
|
|
203
|
+
!!evidence.data.shippingData?.trackingNumber,
|
|
204
|
+
!!evidence.data.shippingData?.address,
|
|
205
|
+
!!evidence.data.interactionData?.sessionDuration,
|
|
206
|
+
!!evidence.data.emailConfirmation?.sentTo,
|
|
207
|
+
!!evidence.data.customerHistory?.accountCreatedAt,
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const passed = checks.filter(Boolean).length;
|
|
211
|
+
return passed / checks.length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ──────────────────────────────────────────
|
|
215
|
+
// FRAUD PROBABILITY
|
|
216
|
+
// ──────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
private _estimateFraudProbability(dispute: Dispute, evidence?: Evidence): number {
|
|
219
|
+
let probability = 0.1;
|
|
220
|
+
|
|
221
|
+
if (dispute.reason === DisputeReason.FRAUDULENT ||
|
|
222
|
+
dispute.reason === DisputeReason.UNAUTHORIZED_TRANSACTION) {
|
|
223
|
+
probability += 0.3;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const ipRep = evidence?.data?.customerData?.ipReputation;
|
|
227
|
+
if (ipRep !== undefined && ipRep < 0.4) {
|
|
228
|
+
probability += 0.25;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const history = evidence?.data?.customerHistory;
|
|
232
|
+
if (history?.previousDisputes && history.previousDisputes > 2) {
|
|
233
|
+
probability += 0.2;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (dispute.amount > 50000) { probability += 0.1; } // > $500
|
|
237
|
+
|
|
238
|
+
return Math.min(1.0, parseFloat(probability.toFixed(4)));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ──────────────────────────────────────────
|
|
242
|
+
// RECOMMENDATION
|
|
243
|
+
// ──────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
private _recommendAction(
|
|
246
|
+
confidence: number,
|
|
247
|
+
fraudProb: number,
|
|
248
|
+
dispute: Dispute
|
|
249
|
+
): 'fight' | 'accept' | 'review' {
|
|
250
|
+
// If confidence is very high → fight
|
|
251
|
+
if (confidence >= 0.75 && fraudProb < 0.4) { return 'fight'; }
|
|
252
|
+
|
|
253
|
+
// If confidence is very low → accept
|
|
254
|
+
if (confidence < 0.30) { return 'accept'; }
|
|
255
|
+
|
|
256
|
+
// Needs manual review
|
|
257
|
+
return 'review';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ──────────────────────────────────────────
|
|
261
|
+
// HUMAN-READABLE REASONS
|
|
262
|
+
// ──────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
private _generateReasons(
|
|
265
|
+
dispute: Dispute,
|
|
266
|
+
evidence: Evidence | undefined,
|
|
267
|
+
scores: Record<string, number>
|
|
268
|
+
): string[] {
|
|
269
|
+
const reasons: string[] = [];
|
|
270
|
+
|
|
271
|
+
if (scores['deliveryProof'] > 0.7) {
|
|
272
|
+
reasons.push('Strong delivery proof with tracking and/or signature available');
|
|
273
|
+
} else if (scores['deliveryProof'] < 0.4) {
|
|
274
|
+
reasons.push('Weak delivery proof — tracking number missing');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (scores['deviceConsistency'] > 0.7) {
|
|
278
|
+
reasons.push('Device fingerprint and IP are consistent across the session');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (scores['customerReputation'] > 0.7) {
|
|
282
|
+
reasons.push('Customer has a clean purchase history with no prior disputes');
|
|
283
|
+
} else if (scores['customerReputation'] < 0.4) {
|
|
284
|
+
reasons.push('Customer has previous disputes on record');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const interaction = evidence?.data?.interactionData;
|
|
288
|
+
if (interaction?.sessionDuration && interaction.sessionDuration > 120) {
|
|
289
|
+
reasons.push(`Long engaged session (${interaction.sessionDuration}s) — indicates genuine purchase`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (dispute.reason === DisputeReason.FRAUDULENT) {
|
|
293
|
+
reasons.push('Dispute claims fraud — device fingerprint comparison is critical evidence');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return reasons;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ──────────────────────────────────────────
|
|
300
|
+
// CONVERTERS
|
|
301
|
+
// ──────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
private _confidenceToRisk(confidence: number): RiskLevel {
|
|
304
|
+
if (confidence >= 0.85) { return RiskLevel.VERY_LOW; }
|
|
305
|
+
if (confidence >= 0.70) { return RiskLevel.LOW; }
|
|
306
|
+
if (confidence >= 0.50) { return RiskLevel.MEDIUM; }
|
|
307
|
+
if (confidence >= 0.30) { return RiskLevel.HIGH; }
|
|
308
|
+
return RiskLevel.CRITICAL;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
confidenceToLevel(score: number): ConfidenceLevel {
|
|
312
|
+
if (score >= 0.85) { return ConfidenceLevel.VERY_HIGH; }
|
|
313
|
+
if (score >= 0.65) { return ConfidenceLevel.HIGH; }
|
|
314
|
+
if (score >= 0.45) { return ConfidenceLevel.MEDIUM; }
|
|
315
|
+
return ConfidenceLevel.LOW;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Bank Integration Abstraction
|
|
3
|
+
// Unified interface over Stripe / PayPal dispute submission
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
import { createLogger } from '../utils/logger';
|
|
7
|
+
import {
|
|
8
|
+
Dispute,
|
|
9
|
+
DisputeReply,
|
|
10
|
+
PaymentProvider,
|
|
11
|
+
} from '../types';
|
|
12
|
+
|
|
13
|
+
const log = createLogger('BankIntegration');
|
|
14
|
+
|
|
15
|
+
// ────────────────────────────────────────────────────────────
|
|
16
|
+
// SUBMISSION RESULT
|
|
17
|
+
// ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface SubmissionResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
disputeId: string;
|
|
22
|
+
status: string;
|
|
23
|
+
submittedAt: string;
|
|
24
|
+
trackingId?: string;
|
|
25
|
+
providerResponse?: unknown;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ────────────────────────────────────────────────────────────
|
|
30
|
+
// ABSTRACT BANK ADAPTER
|
|
31
|
+
// ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export abstract class BankAdapter {
|
|
34
|
+
abstract getProvider(): PaymentProvider;
|
|
35
|
+
abstract submitEvidence(dispute: Dispute, reply: DisputeReply): Promise<SubmissionResult>;
|
|
36
|
+
abstract getDisputeStatus(disputeId: string): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ────────────────────────────────────────────────────────────
|
|
40
|
+
// BANK INTEGRATION (provider dispatcher)
|
|
41
|
+
// ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export class BankIntegration {
|
|
44
|
+
private adapters: Map<PaymentProvider, BankAdapter> = new Map();
|
|
45
|
+
|
|
46
|
+
registerAdapter(adapter: BankAdapter): void {
|
|
47
|
+
this.adapters.set(adapter.getProvider(), adapter);
|
|
48
|
+
log.debug(`Bank adapter registered: ${adapter.getProvider()}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async submitEvidence(dispute: Dispute, reply: DisputeReply): Promise<SubmissionResult> {
|
|
52
|
+
const adapter = this.adapters.get(dispute.provider);
|
|
53
|
+
|
|
54
|
+
if (!adapter) {
|
|
55
|
+
log.warn(`No adapter for provider: ${dispute.provider}`);
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
disputeId: dispute.id,
|
|
59
|
+
status: 'no_adapter',
|
|
60
|
+
submittedAt: new Date().toISOString(),
|
|
61
|
+
error: `No bank adapter registered for provider: ${dispute.provider}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log.info(`Submitting evidence via ${dispute.provider}: ${dispute.id}`);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await adapter.submitEvidence(dispute, reply);
|
|
69
|
+
log.info(`Evidence submitted: ${dispute.id}`, { status: result.status });
|
|
70
|
+
return result;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
log.error(`Evidence submission failed: ${dispute.id}`, { error: message });
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
disputeId: dispute.id,
|
|
77
|
+
status: 'submission_failed',
|
|
78
|
+
submittedAt: new Date().toISOString(),
|
|
79
|
+
error: message,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getDisputeStatus(
|
|
85
|
+
disputeId: string,
|
|
86
|
+
provider: PaymentProvider
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const adapter = this.adapters.get(provider);
|
|
89
|
+
if (!adapter) {
|
|
90
|
+
throw new Error(`No adapter for provider: ${provider}`);
|
|
91
|
+
}
|
|
92
|
+
return adapter.getDisputeStatus(disputeId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
hasAdapter(provider: PaymentProvider): boolean {
|
|
96
|
+
return this.adapters.has(provider);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getSupportedProviders(): PaymentProvider[] {
|
|
100
|
+
return Array.from(this.adapters.keys());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ────────────────────────────────────────────────────────────
|
|
105
|
+
// STRIPE BANK ADAPTER
|
|
106
|
+
// ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export class StripeBankAdapter extends BankAdapter {
|
|
109
|
+
private stripe: unknown;
|
|
110
|
+
|
|
111
|
+
constructor(stripeSecretKey: string) {
|
|
112
|
+
super();
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
114
|
+
const Stripe = require('stripe');
|
|
115
|
+
this.stripe = Stripe(stripeSecretKey);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getProvider(): PaymentProvider {
|
|
119
|
+
return PaymentProvider.STRIPE;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async submitEvidence(dispute: Dispute, reply: DisputeReply): Promise<SubmissionResult> {
|
|
123
|
+
const stripeEvidence = this._mapToStripeEvidence(reply);
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
|
+
const response = await (this.stripe as any).disputes.submitEvidence(
|
|
127
|
+
dispute.id,
|
|
128
|
+
{ evidence: stripeEvidence }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
disputeId: response.id,
|
|
134
|
+
status: response.status,
|
|
135
|
+
submittedAt: new Date().toISOString(),
|
|
136
|
+
trackingId: `stripe-${response.id}-${Date.now()}`,
|
|
137
|
+
providerResponse: response,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getDisputeStatus(disputeId: string): Promise<string> {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
+
const dispute = await (this.stripe as any).disputes.retrieve(disputeId);
|
|
144
|
+
return dispute.status as string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private _mapToStripeEvidence(reply: DisputeReply): Record<string, unknown> {
|
|
148
|
+
const evidence: Record<string, unknown> = {
|
|
149
|
+
uncategorized_text: reply.caseArgument,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const item of reply.evidenceItems) {
|
|
153
|
+
const data = item.data;
|
|
154
|
+
switch (item.type) {
|
|
155
|
+
case 'delivery_proof':
|
|
156
|
+
evidence['shipping_documentation'] = JSON.stringify(data);
|
|
157
|
+
evidence['shipping_tracking_number'] = data['trackingNumber'] ?? '';
|
|
158
|
+
evidence['shipping_date'] = data['shippedAt'] ?? '';
|
|
159
|
+
break;
|
|
160
|
+
case 'device_fingerprint':
|
|
161
|
+
evidence['access_activity_log'] = JSON.stringify(data);
|
|
162
|
+
break;
|
|
163
|
+
case 'customer_communication':
|
|
164
|
+
evidence['customer_communication'] = JSON.stringify(data);
|
|
165
|
+
break;
|
|
166
|
+
case 'email_confirmation':
|
|
167
|
+
evidence['customer_email_address'] = data['sentTo'] ?? '';
|
|
168
|
+
evidence['customer_communication'] = JSON.stringify(data);
|
|
169
|
+
break;
|
|
170
|
+
case 'payment_log':
|
|
171
|
+
evidence['duplicate_charge_documentation'] = JSON.stringify(data);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return evidence;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ────────────────────────────────────────────────────────────
|
|
181
|
+
// PAYPAL BANK ADAPTER
|
|
182
|
+
// ────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export class PayPalBankAdapter extends BankAdapter {
|
|
185
|
+
private clientId: string;
|
|
186
|
+
private clientSecret: string;
|
|
187
|
+
private mode: string;
|
|
188
|
+
private accessToken: string | null = null;
|
|
189
|
+
private tokenExpiry: number = 0;
|
|
190
|
+
|
|
191
|
+
constructor(clientId: string, clientSecret: string, mode = 'sandbox') {
|
|
192
|
+
super();
|
|
193
|
+
this.clientId = clientId;
|
|
194
|
+
this.clientSecret = clientSecret;
|
|
195
|
+
this.mode = mode;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getProvider(): PaymentProvider {
|
|
199
|
+
return PaymentProvider.PAYPAL;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async submitEvidence(dispute: Dispute, reply: DisputeReply): Promise<SubmissionResult> {
|
|
203
|
+
const token = await this._getAccessToken();
|
|
204
|
+
const baseUrl = this.mode === 'live'
|
|
205
|
+
? 'https://api.paypal.com'
|
|
206
|
+
: 'https://api.sandbox.paypal.com';
|
|
207
|
+
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
209
|
+
const axios = require('axios');
|
|
210
|
+
const response = await axios.post(
|
|
211
|
+
`${baseUrl}/v1/customer/disputes/${dispute.id}/provide-evidence`,
|
|
212
|
+
{
|
|
213
|
+
evidences: [{
|
|
214
|
+
evidence_type: 'PROOF_OF_FULFILLMENT',
|
|
215
|
+
notes: reply.caseArgument,
|
|
216
|
+
}],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
headers: {
|
|
220
|
+
Authorization: `Bearer ${token}`,
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
disputeId: dispute.id,
|
|
229
|
+
status: 'submitted',
|
|
230
|
+
submittedAt: new Date().toISOString(),
|
|
231
|
+
providerResponse: response.data,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getDisputeStatus(disputeId: string): Promise<string> {
|
|
236
|
+
const token = await this._getAccessToken();
|
|
237
|
+
const baseUrl = this.mode === 'live'
|
|
238
|
+
? 'https://api.paypal.com'
|
|
239
|
+
: 'https://api.sandbox.paypal.com';
|
|
240
|
+
|
|
241
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
242
|
+
const axios = require('axios');
|
|
243
|
+
const response = await axios.get(
|
|
244
|
+
`${baseUrl}/v1/customer/disputes/${disputeId}`,
|
|
245
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
246
|
+
);
|
|
247
|
+
return (response.data as Record<string, string>)['status'] ?? 'unknown';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async _getAccessToken(): Promise<string> {
|
|
251
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
252
|
+
return this.accessToken;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const baseUrl = this.mode === 'live'
|
|
256
|
+
? 'https://api.paypal.com'
|
|
257
|
+
: 'https://api.sandbox.paypal.com';
|
|
258
|
+
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
260
|
+
const axios = require('axios');
|
|
261
|
+
const response = await axios.post(
|
|
262
|
+
`${baseUrl}/v1/oauth2/token`,
|
|
263
|
+
'grant_type=client_credentials',
|
|
264
|
+
{
|
|
265
|
+
auth: { username: this.clientId, password: this.clientSecret },
|
|
266
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
this.accessToken = (response.data as Record<string, string>)['access_token'];
|
|
271
|
+
this.tokenExpiry = Date.now() + ((response.data as Record<string, number>)['expires_in'] - 60) * 1000;
|
|
272
|
+
return this.accessToken!;
|
|
273
|
+
}
|
|
274
|
+
}
|