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,197 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Evidence Storage
3
+ // Persists, retrieves and manages evidence records securely
4
+ // ============================================================
5
+
6
+ import { createLogger } from '../utils/logger';
7
+ import { Evidence } from '../types';
8
+ import { encryptPIIFields, decryptPIIFields } from './encryption';
9
+
10
+ const log = createLogger('EvidenceStorage');
11
+
12
+ // In-memory store for standalone / testing usage.
13
+ // Production replaces this with the DatabaseManager.
14
+ interface EvidenceRow {
15
+ collectionId: string;
16
+ orderId: string;
17
+ evidenceJson: string;
18
+ trustScore: number;
19
+ riskLevel: string;
20
+ collectedAt: string;
21
+ }
22
+
23
+ // ────────────────────────────────────────────────────────────
24
+ // EVIDENCE STORAGE CLASS
25
+ // ────────────────────────────────────────────────────────────
26
+
27
+ export class EvidenceStorage {
28
+ // Fallback in-memory store (used when DB not yet initialised)
29
+ private readonly memoryStore: Map<string, EvidenceRow> = new Map();
30
+ private db: unknown = null;
31
+
32
+ // Allow the core to inject the DB at runtime
33
+ setDatabase(db: unknown): void {
34
+ this.db = db;
35
+ }
36
+
37
+ // ──────────────────────────────────────────
38
+ // STORE
39
+ // ──────────────────────────────────────────
40
+
41
+ async store(evidence: Evidence): Promise<string> {
42
+ log.debug(`Storing evidence: ${evidence.collectionId}`, {
43
+ orderId: evidence.orderId,
44
+ trustScore: evidence.trustScore,
45
+ });
46
+
47
+ // Encrypt PII before persisting
48
+ const safeCopy = { ...evidence, data: encryptPIIFields(evidence.data as Record<string, unknown>) };
49
+ const json = JSON.stringify(safeCopy);
50
+
51
+ const row: EvidenceRow = {
52
+ collectionId: evidence.collectionId,
53
+ orderId: evidence.orderId,
54
+ evidenceJson: json,
55
+ trustScore: evidence.trustScore,
56
+ riskLevel: evidence.riskLevel,
57
+ collectedAt: evidence.timestamp,
58
+ };
59
+
60
+ if (this.db) {
61
+ try {
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ await (this.db as any).evidence.create(row);
64
+ } catch (err) {
65
+ log.warn('DB store failed, using memory fallback', {
66
+ error: err instanceof Error ? err.message : String(err),
67
+ });
68
+ this.memoryStore.set(evidence.orderId, row);
69
+ }
70
+ } else {
71
+ this.memoryStore.set(evidence.orderId, row);
72
+ }
73
+
74
+ log.info(`Evidence stored: ${evidence.collectionId} for order: ${evidence.orderId}`);
75
+ return evidence.collectionId;
76
+ }
77
+
78
+ // ──────────────────────────────────────────
79
+ // FIND BY ORDER ID
80
+ // ──────────────────────────────────────────
81
+
82
+ async findByOrderId(orderId: string): Promise<Evidence | null> {
83
+ log.debug(`Looking up evidence for order: ${orderId}`);
84
+
85
+ let row: EvidenceRow | null = null;
86
+
87
+ if (this.db) {
88
+ try {
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ row = await (this.db as any).evidence.findByOrderId(orderId);
91
+ } catch {
92
+ row = this.memoryStore.get(orderId) ?? null;
93
+ }
94
+ } else {
95
+ row = this.memoryStore.get(orderId) ?? null;
96
+ }
97
+
98
+ if (!row) {
99
+ log.warn(`No evidence found for order: ${orderId}`);
100
+ return null;
101
+ }
102
+
103
+ return this._deserialize(row);
104
+ }
105
+
106
+ // ──────────────────────────────────────────
107
+ // FIND BY COLLECTION ID
108
+ // ──────────────────────────────────────────
109
+
110
+ async findByCollectionId(collectionId: string): Promise<Evidence | null> {
111
+ if (this.db) {
112
+ try {
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ const row = await (this.db as any).evidence.findByCollectionId(collectionId);
115
+ return row ? this._deserialize(row) : null;
116
+ } catch {
117
+ // fall through to memory
118
+ }
119
+ }
120
+
121
+ for (const row of this.memoryStore.values()) {
122
+ if (row.collectionId === collectionId) {
123
+ return this._deserialize(row);
124
+ }
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ // ──────────────────────────────────────────
131
+ // UPDATE
132
+ // ──────────────────────────────────────────
133
+
134
+ async update(orderId: string, updates: Partial<Evidence>): Promise<boolean> {
135
+ const existing = await this.findByOrderId(orderId);
136
+ if (!existing) { return false; }
137
+
138
+ const merged: Evidence = { ...existing, ...updates };
139
+ await this.store(merged);
140
+ return true;
141
+ }
142
+
143
+ // ──────────────────────────────────────────
144
+ // DELETE (GDPR right to erasure)
145
+ // ──────────────────────────────────────────
146
+
147
+ async delete(orderId: string): Promise<boolean> {
148
+ log.info(`Deleting evidence for order: ${orderId}`);
149
+
150
+ if (this.db) {
151
+ try {
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ await (this.db as any).evidence.deleteByOrderId(orderId);
154
+ } catch {
155
+ this.memoryStore.delete(orderId);
156
+ }
157
+ } else {
158
+ this.memoryStore.delete(orderId);
159
+ }
160
+
161
+ return true;
162
+ }
163
+
164
+ // ──────────────────────────────────────────
165
+ // LIST (paginated)
166
+ // ──────────────────────────────────────────
167
+
168
+ async list(
169
+ page = 1,
170
+ limit = 20
171
+ ): Promise<{ items: Evidence[]; total: number }> {
172
+ const all = Array.from(this.memoryStore.values());
173
+ const start = (page - 1) * limit;
174
+ const slice = all.slice(start, start + limit);
175
+
176
+ return {
177
+ items: slice.map(r => this._deserialize(r)).filter(Boolean) as Evidence[],
178
+ total: all.length,
179
+ };
180
+ }
181
+
182
+ // ──────────────────────────────────────────
183
+ // HELPERS
184
+ // ──────────────────────────────────────────
185
+
186
+ private _deserialize(row: EvidenceRow): Evidence {
187
+ const parsed: Evidence = JSON.parse(row.evidenceJson);
188
+ // Decrypt PII
189
+ parsed.data = decryptPIIFields(parsed.data as Record<string, unknown>) as Evidence['data'];
190
+ return parsed;
191
+ }
192
+
193
+ // Expose store size for health checks
194
+ get size(): number {
195
+ return this.memoryStore.size;
196
+ }
197
+ }
@@ -0,0 +1,184 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Evidence Validator
3
+ // ============================================================
4
+
5
+ import { z } from 'zod';
6
+ import { createLogger } from '../utils/logger';
7
+ import { Evidence, RiskLevel } from '../types';
8
+
9
+ const log = createLogger('EvidenceValidator');
10
+
11
+ // ────────────────────────────────────────────────────────────
12
+ // VALIDATION SCHEMAS
13
+ // ────────────────────────────────────────────────────────────
14
+
15
+ const ipSchema = z.string().ip().optional().or(z.literal('0.0.0.0')).optional();
16
+
17
+ const evidenceDataSchema = z.object({
18
+ customerData: z.object({
19
+ ipAddress: z.string().default('0.0.0.0'),
20
+ userAgent: z.string().default('Unknown'),
21
+ deviceId: z.string(),
22
+ email: z.string().email().optional(),
23
+ ipReputation: z.number().min(0).max(1).optional(),
24
+ }),
25
+ transactionData: z.object({
26
+ timestamp: z.string(),
27
+ amount: z.number().min(0),
28
+ currency: z.string().length(3),
29
+ transactionId: z.string(),
30
+ }),
31
+ shippingData: z.object({
32
+ trackingNumber: z.string().optional(),
33
+ address: z.any().optional(),
34
+ }).optional(),
35
+ interactionData: z.object({
36
+ sessionDuration: z.number().min(0),
37
+ pageViews: z.array(z.any()).default([]),
38
+ clickTracking: z.array(z.any()).default([]),
39
+ formInteractions: z.array(z.any()).default([]),
40
+ timeOnCheckout: z.number().min(0),
41
+ }).optional(),
42
+ }).passthrough();
43
+
44
+ const evidenceSchema = z.object({
45
+ collectionId: z.string().min(1),
46
+ orderId: z.string().min(1),
47
+ timestamp: z.string(),
48
+ trustScore: z.number().min(0).max(100),
49
+ riskLevel: z.nativeEnum(RiskLevel),
50
+ data: evidenceDataSchema,
51
+ });
52
+
53
+ // ────────────────────────────────────────────────────────────
54
+ // VALIDATION RESULT
55
+ // ────────────────────────────────────────────────────────────
56
+
57
+ export interface ValidationResult {
58
+ valid: boolean;
59
+ errors: string[];
60
+ warnings: string[];
61
+ sanitizedEvidence: Evidence;
62
+ }
63
+
64
+ // ────────────────────────────────────────────────────────────
65
+ // EVIDENCE VALIDATOR CLASS
66
+ // ────────────────────────────────────────────────────────────
67
+
68
+ export class EvidenceValidator {
69
+
70
+ async validate(evidence: Evidence): Promise<Evidence> {
71
+ log.debug(`Validating evidence: ${evidence.collectionId}`);
72
+
73
+ const result = await this.validateDetailed(evidence);
74
+
75
+ if (!result.valid && result.errors.length > 0) {
76
+ log.warn(`Evidence validation issues: ${evidence.collectionId}`, {
77
+ errors: result.errors,
78
+ warnings: result.warnings,
79
+ });
80
+ }
81
+
82
+ return result.sanitizedEvidence;
83
+ }
84
+
85
+ async validateDetailed(evidence: Evidence): Promise<ValidationResult> {
86
+ const errors: string[] = [];
87
+ const warnings: string[] = [];
88
+
89
+ // ── Schema validation ──
90
+ try {
91
+ evidenceSchema.parse(evidence);
92
+ } catch (err) {
93
+ if (err instanceof z.ZodError) {
94
+ err.errors.forEach(e => errors.push(`${e.path.join('.')}: ${e.message}`));
95
+ }
96
+ }
97
+
98
+ // ── Business-rule validations ──
99
+ this._validateBusinessRules(evidence, errors, warnings);
100
+
101
+ // ── Sanitize ──
102
+ const sanitizedEvidence = this._sanitize(evidence);
103
+
104
+ return {
105
+ valid: errors.length === 0,
106
+ errors,
107
+ warnings,
108
+ sanitizedEvidence,
109
+ };
110
+ }
111
+
112
+ // ──────────────────────────────────────────
113
+ // BUSINESS RULE VALIDATION
114
+ // ──────────────────────────────────────────
115
+
116
+ private _validateBusinessRules(
117
+ ev: Evidence,
118
+ errors: string[],
119
+ warnings: string[]
120
+ ): void {
121
+ // Trust score range
122
+ if (ev.trustScore < 0 || ev.trustScore > 100) {
123
+ errors.push(`trustScore out of range: ${ev.trustScore}`);
124
+ }
125
+
126
+ // Timestamp must be recent (within 24 hours)
127
+ const ts = new Date(ev.timestamp).getTime();
128
+ const now = Date.now();
129
+ if (isNaN(ts)) {
130
+ errors.push('timestamp is not a valid date');
131
+ } else if (now - ts > 86400000 * 2) {
132
+ warnings.push('Evidence timestamp is older than 48 hours');
133
+ }
134
+
135
+ // Customer IP warning
136
+ const ip = ev.data?.customerData?.ipAddress;
137
+ if (!ip || ip === '0.0.0.0') {
138
+ warnings.push('No customer IP address recorded — weakens defense');
139
+ }
140
+
141
+ // Shipping tracking warning
142
+ if (!ev.data?.shippingData?.trackingNumber) {
143
+ warnings.push('No shipping tracking number — weakens physical delivery proof');
144
+ }
145
+
146
+ // Low trust score warning
147
+ if (ev.trustScore < 40) {
148
+ warnings.push(`Low trust score (${ev.trustScore}) — consider manual review`);
149
+ }
150
+
151
+ // Critical trust score
152
+ if (ev.trustScore < 20) {
153
+ errors.push(`Critically low trust score (${ev.trustScore}) — evidence insufficient`);
154
+ }
155
+ }
156
+
157
+ // ──────────────────────────────────────────
158
+ // SANITIZER
159
+ // ──────────────────────────────────────────
160
+
161
+ private _sanitize(evidence: Evidence): Evidence {
162
+ const sanitized: Evidence = JSON.parse(JSON.stringify(evidence)); // deep clone
163
+
164
+ // Clamp score
165
+ sanitized.trustScore = Math.max(0, Math.min(100, sanitized.trustScore));
166
+
167
+ // Remove undefined values
168
+ this._removeUndefined(sanitized);
169
+
170
+ return sanitized;
171
+ }
172
+
173
+ private _removeUndefined(obj: unknown): void {
174
+ if (typeof obj !== 'object' || obj === null) { return; }
175
+ for (const key of Object.keys(obj as Record<string, unknown>)) {
176
+ const val = (obj as Record<string, unknown>)[key];
177
+ if (val === undefined) {
178
+ delete (obj as Record<string, unknown>)[key];
179
+ } else {
180
+ this._removeUndefined(val);
181
+ }
182
+ }
183
+ }
184
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Public Package Entry Point
3
+ // ============================================================
4
+
5
+ export { ChargebackGuard } from './core/chargebackGuard';
6
+ export { ChargebackEventEmitter } from './core/eventEmitter';
7
+ export { LifecycleManager } from './core/lifecycle';
8
+
9
+ // Types — everything consumers need
10
+ export * from './types';
11
+
12
+ // Config export (readonly)
13
+ export { default as config } from './config';
14
+
15
+ // Named re-exports for convenience
16
+ export type { RegistrationResult, DisputeHandlingResult } from './core/chargebackGuard';
17
+ export type { HealthCheck, LifecycleStatus } from './core/lifecycle';
18
+
19
+ // ────────────────────────────────────────────────────────────
20
+ // DEFAULT FACTORY (quick-start)
21
+ // ────────────────────────────────────────────────────────────
22
+
23
+ import { ChargebackGuard } from './core/chargebackGuard';
24
+ import { ChargebackGuardConfig } from './types';
25
+
26
+ /**
27
+ * Create and initialize a ChargebackGuard instance in one call.
28
+ *
29
+ * @example
30
+ * const guard = await createChargebackGuard({
31
+ * apiKey: process.env.STRIPE_SECRET_KEY!,
32
+ * webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
33
+ * });
34
+ */
35
+ export async function createChargebackGuard(
36
+ options: Partial<ChargebackGuardConfig> = {}
37
+ ): Promise<ChargebackGuard> {
38
+ const guard = new ChargebackGuard(options);
39
+ await guard.initialize();
40
+ return guard;
41
+ }
42
+
43
+ export default ChargebackGuard;
@@ -0,0 +1,258 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — PayPal Integration (Full)
3
+ // ============================================================
4
+
5
+ import axios, { AxiosInstance } from 'axios';
6
+ import { createLogger } from '../utils/logger';
7
+ import { paypalConfig } from '../config';
8
+ import {
9
+ Dispute,
10
+ DisputeReply,
11
+ DisputeReason,
12
+ DisputeStatus,
13
+ PaymentProvider,
14
+ } from '../types';
15
+
16
+ const log = createLogger('PayPalIntegration');
17
+
18
+ interface PayPalTokenResponse {
19
+ access_token: string;
20
+ token_type: string;
21
+ expires_in: number;
22
+ }
23
+
24
+ // ────────────────────────────────────────────────────────────
25
+ // PAYPAL INTEGRATION CLASS
26
+ // ────────────────────────────────────────────────────────────
27
+
28
+ export class PayPalIntegration {
29
+ private readonly baseUrl: string;
30
+ private readonly clientId: string;
31
+ private readonly clientSecret: string;
32
+ private accessToken: string | null = null;
33
+ private tokenExpiry: number = 0;
34
+ private readonly http: AxiosInstance;
35
+
36
+ constructor(
37
+ clientId?: string,
38
+ clientSecret?: string,
39
+ mode?: 'sandbox' | 'live'
40
+ ) {
41
+ this.clientId = clientId ?? paypalConfig.clientId;
42
+ this.clientSecret = clientSecret ?? paypalConfig.clientSecret;
43
+ const env = mode ?? paypalConfig.mode;
44
+
45
+ this.baseUrl = env === 'live'
46
+ ? 'https://api.paypal.com'
47
+ : 'https://api.sandbox.paypal.com';
48
+
49
+ this.http = axios.create({
50
+ baseURL: this.baseUrl,
51
+ timeout: 30000,
52
+ });
53
+
54
+ log.debug(`PayPal client initialized (${env})`);
55
+ }
56
+
57
+ // ──────────────────────────────────────────
58
+ // AUTH
59
+ // ──────────────────────────────────────────
60
+
61
+ async authenticate(): Promise<string> {
62
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
63
+ return this.accessToken;
64
+ }
65
+
66
+ const response = await axios.post<PayPalTokenResponse>(
67
+ `${this.baseUrl}/v1/oauth2/token`,
68
+ 'grant_type=client_credentials',
69
+ {
70
+ auth: { username: this.clientId, password: this.clientSecret },
71
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
72
+ }
73
+ );
74
+
75
+ this.accessToken = response.data.access_token;
76
+ this.tokenExpiry = Date.now() + (response.data.expires_in - 60) * 1000;
77
+ log.debug('PayPal access token refreshed');
78
+ return this.accessToken;
79
+ }
80
+
81
+ private async _authHeaders(): Promise<Record<string, string>> {
82
+ const token = await this.authenticate();
83
+ return {
84
+ Authorization: `Bearer ${token}`,
85
+ 'Content-Type': 'application/json',
86
+ };
87
+ }
88
+
89
+ // ──────────────────────────────────────────
90
+ // GET DISPUTE
91
+ // ──────────────────────────────────────────
92
+
93
+ async getDispute(disputeId: string): Promise<Dispute> {
94
+ log.debug(`Fetching PayPal dispute: ${disputeId}`);
95
+ const headers = await this._authHeaders();
96
+ const response = await this.http.get(`/v1/customer/disputes/${disputeId}`, { headers });
97
+ return this._mapDisputeResponse(response.data as Record<string, unknown>);
98
+ }
99
+
100
+ // ──────────────────────────────────────────
101
+ // LIST DISPUTES
102
+ // ──────────────────────────────────────────
103
+
104
+ async listDisputes(params?: { status?: string; limit?: number }): Promise<Dispute[]> {
105
+ const headers = await this._authHeaders();
106
+ const response = await this.http.get('/v1/customer/disputes', {
107
+ headers,
108
+ params: {
109
+ dispute_state: params?.status ?? 'OPEN',
110
+ page_size: params?.limit ?? 50,
111
+ },
112
+ });
113
+
114
+ const data = response.data as Record<string, unknown>;
115
+ const items = (data['items'] as Record<string, unknown>[]) ?? [];
116
+ return items.map(item => this._mapDisputeResponse(item));
117
+ }
118
+
119
+ // ──────────────────────────────────────────
120
+ // SUBMIT EVIDENCE
121
+ // ──────────────────────────────────────────
122
+
123
+ async submitDisputeEvidence(disputeId: string, reply: DisputeReply): Promise<void> {
124
+ log.info(`Submitting PayPal dispute evidence: ${disputeId}`);
125
+ const headers = await this._authHeaders();
126
+
127
+ const evidencePayload = {
128
+ evidences: reply.evidenceItems.map(item => ({
129
+ evidence_type: this._mapEvidenceType(item.type as string),
130
+ notes: `${item.description}\n\n${JSON.stringify(item.data, null, 2)}`,
131
+ })),
132
+ note: reply.caseArgument,
133
+ };
134
+
135
+ await this.http.post(
136
+ `/v1/customer/disputes/${disputeId}/provide-evidence`,
137
+ evidencePayload,
138
+ { headers }
139
+ );
140
+
141
+ log.info(`PayPal evidence submitted: ${disputeId}`);
142
+ }
143
+
144
+ // ──────────────────────────────────────────
145
+ // ACCEPT DISPUTE (when appropriate)
146
+ // ──────────────────────────────────────────
147
+
148
+ async acceptDispute(disputeId: string, note?: string): Promise<void> {
149
+ const headers = await this._authHeaders();
150
+ await this.http.post(
151
+ `/v1/customer/disputes/${disputeId}/accept-dispute`,
152
+ { note: note ?? 'Accepting dispute per merchant decision.' },
153
+ { headers }
154
+ );
155
+ }
156
+
157
+ // ──────────────────────────────────────────
158
+ // APPEAL DISPUTE
159
+ // ──────────────────────────────────────────
160
+
161
+ async appealDispute(disputeId: string, note: string): Promise<void> {
162
+ const headers = await this._authHeaders();
163
+ await this.http.post(
164
+ `/v1/customer/disputes/${disputeId}/appeal`,
165
+ { evidences: [{ evidence_type: 'PROOF_OF_FULFILLMENT', notes: note }] },
166
+ { headers }
167
+ );
168
+ }
169
+
170
+ // ──────────────────────────────────────────
171
+ // WEBHOOK VERIFICATION
172
+ // ──────────────────────────────────────────
173
+
174
+ async verifyWebhookSignature(
175
+ headers: Record<string, string>,
176
+ body: string
177
+ ): Promise<boolean> {
178
+ const authToken = await this.authenticate();
179
+ try {
180
+ const response = await this.http.post(
181
+ '/v1/notifications/verify-webhook-signature',
182
+ {
183
+ auth_algo: headers['paypal-auth-algo'],
184
+ cert_url: headers['paypal-cert-url'],
185
+ transmission_id: headers['paypal-transmission-id'],
186
+ transmission_sig: headers['paypal-transmission-sig'],
187
+ transmission_time: headers['paypal-transmission-time'],
188
+ webhook_id: paypalConfig.webhookId,
189
+ webhook_event: JSON.parse(body),
190
+ },
191
+ { headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' } }
192
+ );
193
+ return (response.data as Record<string, string>)['verification_status'] === 'SUCCESS';
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ // ──────────────────────────────────────────
200
+ // HELPERS
201
+ // ──────────────────────────────────────────
202
+
203
+ private _mapDisputeResponse(data: Record<string, unknown>): Dispute {
204
+ const disputeAmount = (data['dispute_amount'] as Record<string, string> | undefined);
205
+ const amountValue = disputeAmount?.['value'] ? parseFloat(disputeAmount.value) * 100 : 0;
206
+
207
+ return {
208
+ id: String(data['dispute_id'] ?? ''),
209
+ orderId: this._extractOrderId(data),
210
+ amount: Math.round(amountValue),
211
+ currency: (disputeAmount?.['currency_code']) ?? 'USD',
212
+ reason: this._mapReason(String(data['reason'] ?? 'OTHER')),
213
+ status: this._mapStatus(String(data['status'] ?? 'OPEN')),
214
+ provider: PaymentProvider.PAYPAL,
215
+ createdAt: String(data['create_time'] ?? new Date().toISOString()),
216
+ metadata: { raw: data },
217
+ };
218
+ }
219
+
220
+ private _extractOrderId(data: Record<string, unknown>): string | undefined {
221
+ const txns = data['disputed_transactions'] as Array<Record<string, string>> | undefined;
222
+ return txns?.[0]?.['seller_transaction_id'];
223
+ }
224
+
225
+ private _mapReason(reason: string): DisputeReason {
226
+ const map: Record<string, DisputeReason> = {
227
+ MERCHANDISE_OR_SERVICE_NOT_RECEIVED: DisputeReason.PRODUCT_NOT_RECEIVED,
228
+ MERCHANDISE_OR_SERVICE_NOT_AS_DESCRIBED: DisputeReason.PRODUCT_UNACCEPTABLE,
229
+ UNAUTHORISED: DisputeReason.UNAUTHORIZED_TRANSACTION,
230
+ CREDIT_NOT_PROCESSED: DisputeReason.CREDIT_NOT_PROCESSED,
231
+ DUPLICATE_TRANSACTION: DisputeReason.DUPLICATE_TRANSACTION,
232
+ CANCELED_RECURRING_BILLING: DisputeReason.SUBSCRIPTION_CANCELLED,
233
+ };
234
+ return map[reason] ?? DisputeReason.GENERAL;
235
+ }
236
+
237
+ private _mapStatus(status: string): DisputeStatus {
238
+ const map: Record<string, DisputeStatus> = {
239
+ OPEN: DisputeStatus.NEEDS_RESPONSE,
240
+ UNDER_REVIEW: DisputeStatus.UNDER_REVIEW,
241
+ RESOLVED: DisputeStatus.CHARGE_REFUNDED,
242
+ WAITING_FOR_BUYER_RESPONSE: DisputeStatus.UNDER_REVIEW,
243
+ WAITING_FOR_SELLER_RESPONSE: DisputeStatus.NEEDS_RESPONSE,
244
+ };
245
+ return map[status] ?? DisputeStatus.NEEDS_RESPONSE;
246
+ }
247
+
248
+ private _mapEvidenceType(type: string): string {
249
+ const map: Record<string, string> = {
250
+ delivery_proof: 'PROOF_OF_FULFILLMENT',
251
+ device_fingerprint: 'PROOF_OF_FULFILLMENT',
252
+ customer_history: 'PROOF_OF_REFUND',
253
+ payment_log: 'PROOF_OF_FULFILLMENT',
254
+ email_confirmation: 'PROOF_OF_FULFILLMENT',
255
+ };
256
+ return map[type] ?? 'PROOF_OF_FULFILLMENT';
257
+ }
258
+ }