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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/docs/api.md +278 -0
  4. package/docs/architecture.md +281 -0
  5. package/docs/configuration.md +292 -0
  6. package/docs/getting-started.md +155 -0
  7. package/examples/advancedConfig.ts +123 -0
  8. package/examples/basicUsage.ts +98 -0
  9. package/examples/stripeIntegration.ts +106 -0
  10. package/package.json +181 -0
  11. package/src/ai/fraudDetection.ts +261 -0
  12. package/src/ai/patternRecognition.ts +218 -0
  13. package/src/analytics/dashboard.ts +195 -0
  14. package/src/analytics/metrics.ts +175 -0
  15. package/src/analytics/predictions.ts +135 -0
  16. package/src/analytics/reports.ts +221 -0
  17. package/src/api/controllers.ts +339 -0
  18. package/src/api/middleware.ts +172 -0
  19. package/src/api/routes.ts +141 -0
  20. package/src/config.ts +231 -0
  21. package/src/core/chargebackGuard.ts +616 -0
  22. package/src/core/eventEmitter.ts +118 -0
  23. package/src/core/lifecycle.ts +215 -0
  24. package/src/database/schema.ts +392 -0
  25. package/src/dispute/analyzer.ts +317 -0
  26. package/src/dispute/bankIntegration.ts +274 -0
  27. package/src/dispute/detector.ts +239 -0
  28. package/src/dispute/responseEngine.ts +440 -0
  29. package/src/evidence/collector.ts +426 -0
  30. package/src/evidence/encryption.ts +168 -0
  31. package/src/evidence/storage.ts +197 -0
  32. package/src/evidence/validator.ts +184 -0
  33. package/src/index.ts +43 -0
  34. package/src/integrations/paypal.ts +258 -0
  35. package/src/integrations/stripe.ts +280 -0
  36. package/src/integrations/webhook.ts +332 -0
  37. package/src/notifications/email.ts +161 -0
  38. package/src/notifications/inApp.ts +319 -0
  39. package/src/notifications/sms.ts +58 -0
  40. package/src/security/auth.ts +153 -0
  41. package/src/security/rateLimit.ts +77 -0
  42. package/src/security/validation.ts +166 -0
  43. package/src/server.ts +122 -0
  44. package/src/types/index.ts +790 -0
  45. package/src/utils/formatters.ts +72 -0
  46. package/src/utils/helpers.ts +193 -0
  47. package/src/utils/logger.ts +88 -0
  48. 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
+ }