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,280 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Stripe Integration (Full)
3
+ // ============================================================
4
+
5
+ import Stripe from 'stripe';
6
+ import { createLogger } from '../utils/logger';
7
+ import { stripeConfig } from '../config';
8
+ import {
9
+ Dispute,
10
+ DisputeReply,
11
+ DisputeStatus,
12
+ DisputeReason,
13
+ PaymentProvider,
14
+ } from '../types';
15
+
16
+ const log = createLogger('StripeIntegration');
17
+
18
+ // ────────────────────────────────────────────────────────────
19
+ // DISPUTE STATUS / REASON MAPS
20
+ // ────────────────────────────────────────────────────────────
21
+
22
+ const STATUS_MAP: Record<string, DisputeStatus> = {
23
+ needs_response: DisputeStatus.NEEDS_RESPONSE,
24
+ under_review: DisputeStatus.UNDER_REVIEW,
25
+ charge_refunded: DisputeStatus.CHARGE_REFUNDED,
26
+ won: DisputeStatus.WON,
27
+ lost: DisputeStatus.LOST,
28
+ warning_needs_response: DisputeStatus.WARNING_NEEDS_RESPONSE,
29
+ warning_under_review: DisputeStatus.WARNING_UNDER_REVIEW,
30
+ warning_closed: DisputeStatus.WARNING_CLOSED,
31
+ };
32
+
33
+ const REASON_MAP: Record<string, DisputeReason> = {
34
+ product_not_received: DisputeReason.PRODUCT_NOT_RECEIVED,
35
+ product_unacceptable: DisputeReason.PRODUCT_UNACCEPTABLE,
36
+ fraudulent: DisputeReason.FRAUDULENT,
37
+ unrecognized: DisputeReason.UNAUTHORIZED_TRANSACTION,
38
+ duplicate: DisputeReason.DUPLICATE_TRANSACTION,
39
+ credit_not_processed: DisputeReason.CREDIT_NOT_PROCESSED,
40
+ subscription_canceled: DisputeReason.SUBSCRIPTION_CANCELLED,
41
+ general: DisputeReason.GENERAL,
42
+ };
43
+
44
+ // ────────────────────────────────────────────────────────────
45
+ // STRIPE INTEGRATION CLASS
46
+ // ────────────────────────────────────────────────────────────
47
+
48
+ export class StripeIntegration {
49
+ private readonly client: Stripe;
50
+
51
+ constructor(apiKey?: string) {
52
+ const key = apiKey ?? stripeConfig.secretKey;
53
+ if (!key) {
54
+ throw new Error('Stripe API key is required');
55
+ }
56
+ this.client = new Stripe(key, {
57
+ apiVersion: '2023-10-16' as Stripe.LatestApiVersion,
58
+ typescript: true,
59
+ maxNetworkRetries: 3,
60
+ });
61
+ log.debug('Stripe client initialized');
62
+ }
63
+
64
+ // ──────────────────────────────────────────
65
+ // WEBHOOK VERIFICATION
66
+ // ──────────────────────────────────────────
67
+
68
+ verifyWebhookSignature(payload: string | Buffer, signature: string): Stripe.Event {
69
+ return this.client.webhooks.constructEvent(
70
+ payload,
71
+ signature,
72
+ stripeConfig.webhookSecret
73
+ );
74
+ }
75
+
76
+ // ──────────────────────────────────────────
77
+ // GET DISPUTE
78
+ // ──────────────────────────────────────────
79
+
80
+ async getDispute(disputeId: string): Promise<Dispute> {
81
+ log.debug(`Fetching dispute: ${disputeId}`);
82
+ const d = await this.client.disputes.retrieve(disputeId);
83
+
84
+ return {
85
+ id: d.id,
86
+ orderId: (d.metadata as Record<string, string>)['order_id']
87
+ ?? (d.metadata as Record<string, string>)['orderId']
88
+ ?? undefined,
89
+ amount: d.amount,
90
+ currency: d.currency,
91
+ reason: REASON_MAP[d.reason] ?? DisputeReason.GENERAL,
92
+ status: STATUS_MAP[d.status] ?? DisputeStatus.NEEDS_RESPONSE,
93
+ provider: PaymentProvider.STRIPE,
94
+ evidenceDueBy: d.evidence_due_by
95
+ ? new Date(d.evidence_due_by * 1000).toISOString()
96
+ : undefined,
97
+ createdAt: new Date(d.created * 1000).toISOString(),
98
+ metadata: d.metadata as Record<string, unknown>,
99
+ };
100
+ }
101
+
102
+ // ──────────────────────────────────────────
103
+ // LIST DISPUTES
104
+ // ──────────────────────────────────────────
105
+
106
+ async listDisputes(params?: {
107
+ status?: string;
108
+ limit?: number;
109
+ startingAfter?: string;
110
+ }): Promise<Dispute[]> {
111
+ const list = await this.client.disputes.list({
112
+ limit: params?.limit ?? 100,
113
+ starting_after: params?.startingAfter,
114
+ });
115
+
116
+ return list.data.map(d => ({
117
+ id: d.id,
118
+ orderId: (d.metadata as Record<string, string>)['order_id'] ?? undefined,
119
+ amount: d.amount,
120
+ currency: d.currency,
121
+ reason: REASON_MAP[d.reason] ?? DisputeReason.GENERAL,
122
+ status: STATUS_MAP[d.status] ?? DisputeStatus.NEEDS_RESPONSE,
123
+ provider: PaymentProvider.STRIPE,
124
+ evidenceDueBy: d.evidence_due_by
125
+ ? new Date(d.evidence_due_by * 1000).toISOString()
126
+ : undefined,
127
+ createdAt: new Date(d.created * 1000).toISOString(),
128
+ metadata: d.metadata as Record<string, unknown>,
129
+ }));
130
+ }
131
+
132
+ // ──────────────────────────────────────────
133
+ // LIST ACTIONABLE DISPUTES
134
+ // ──────────────────────────────────────────
135
+
136
+ async listActionableDisputes(): Promise<Dispute[]> {
137
+ const [needs, warning] = await Promise.all([
138
+ this.listDisputes({ status: 'needs_response', limit: 100 }),
139
+ this.listDisputes({ status: 'warning_needs_response', limit: 100 }),
140
+ ]);
141
+ return [...needs, ...warning];
142
+ }
143
+
144
+ // ──────────────────────────────────────────
145
+ // SUBMIT EVIDENCE
146
+ // ──────────────────────────────────────────
147
+
148
+ async submitDisputeEvidence(disputeId: string, reply: DisputeReply): Promise<Stripe.Dispute> {
149
+ log.info(`Submitting evidence for dispute: ${disputeId}`);
150
+
151
+ const evidencePayload = this._buildEvidencePayload(reply);
152
+
153
+ const result = await this.client.disputes.update(disputeId, {
154
+ evidence: evidencePayload,
155
+ submit: true,
156
+ });
157
+
158
+ log.info(`Evidence submitted for dispute: ${disputeId}`, { status: result.status });
159
+ return result;
160
+ }
161
+
162
+ // ──────────────────────────────────────────
163
+ // UPDATE DISPUTE METADATA
164
+ // ──────────────────────────────────────────
165
+
166
+ async updateDisputeMetadata(
167
+ disputeId: string,
168
+ metadata: Record<string, string>
169
+ ): Promise<void> {
170
+ await this.client.disputes.update(disputeId, { metadata });
171
+ }
172
+
173
+ // ──────────────────────────────────────────
174
+ // GET DISPUTE STATS
175
+ // ──────────────────────────────────────────
176
+
177
+ async getDisputeStats(): Promise<{
178
+ total: number;
179
+ byStatus: Record<string, number>;
180
+ byReason: Record<string, number>;
181
+ totalAmount: number;
182
+ }> {
183
+ const all = await this.client.disputes.list({ limit: 100 });
184
+
185
+ const stats = {
186
+ total: 0,
187
+ byStatus: {} as Record<string, number>,
188
+ byReason: {} as Record<string, number>,
189
+ totalAmount: 0,
190
+ };
191
+
192
+ for (const d of all.data) {
193
+ stats.total++;
194
+ stats.totalAmount += d.amount;
195
+ stats.byStatus[d.status] = (stats.byStatus[d.status] ?? 0) + 1;
196
+ stats.byReason[d.reason] = (stats.byReason[d.reason] ?? 0) + 1;
197
+ }
198
+
199
+ return stats;
200
+ }
201
+
202
+ // ──────────────────────────────────────────
203
+ // RETRIEVE CHARGE
204
+ // ──────────────────────────────────────────
205
+
206
+ async getCharge(chargeId: string): Promise<Stripe.Charge> {
207
+ return this.client.charges.retrieve(chargeId);
208
+ }
209
+
210
+ // ──────────────────────────────────────────
211
+ // RETRIEVE PAYMENT INTENT
212
+ // ──────────────────────────────────────────
213
+
214
+ async getPaymentIntent(piId: string): Promise<Stripe.PaymentIntent> {
215
+ return this.client.paymentIntents.retrieve(piId, {
216
+ expand: ['payment_method', 'latest_charge'],
217
+ });
218
+ }
219
+
220
+ // ──────────────────────────────────────────
221
+ // EVIDENCE PAYLOAD BUILDER
222
+ // ──────────────────────────────────────────
223
+
224
+ private _buildEvidencePayload(reply: DisputeReply): Stripe.DisputeUpdateParams.Evidence {
225
+ const payload: Stripe.DisputeUpdateParams.Evidence = {
226
+ uncategorized_text: reply.caseArgument,
227
+ };
228
+
229
+ for (const item of reply.evidenceItems) {
230
+ const d = item.data;
231
+ switch (item.type) {
232
+ case 'delivery_proof':
233
+ if (d['trackingNumber']) {
234
+ payload.shipping_tracking_number = String(d['trackingNumber']);
235
+ }
236
+ if (d['shippingAddress']) {
237
+ payload.shipping_address = JSON.stringify(d['shippingAddress']);
238
+ }
239
+ if (d['shippedAt']) {
240
+ payload.shipping_date = String(d['shippedAt']);
241
+ }
242
+ payload.shipping_documentation = JSON.stringify(d);
243
+ break;
244
+
245
+ case 'device_fingerprint':
246
+ case 'behavior_analysis':
247
+ payload.access_activity_log = JSON.stringify(d);
248
+ break;
249
+
250
+ case 'customer_communication':
251
+ case 'email_confirmation':
252
+ if (d['sentTo']) {
253
+ payload.customer_email_address = String(d['sentTo']);
254
+ }
255
+ payload.customer_communication = JSON.stringify(d);
256
+ break;
257
+
258
+ case 'customer_history':
259
+ payload.customer_name = String(d['customerId'] ?? '');
260
+ break;
261
+
262
+ case 'payment_log':
263
+ payload.duplicate_charge_explanation = JSON.stringify(d);
264
+ break;
265
+
266
+ case 'refund_policy':
267
+ payload.refund_policy = String(d['policy'] ?? '');
268
+ payload.refund_policy_disclosure = String(d['note'] ?? '');
269
+ break;
270
+
271
+ case 'subscription_terms':
272
+ payload.cancellation_policy = String(d['cancellationPolicy'] ?? '');
273
+ payload.cancellation_policy_disclosure = String(d['note'] ?? '');
274
+ break;
275
+ }
276
+ }
277
+
278
+ return payload;
279
+ }
280
+ }
@@ -0,0 +1,332 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Webhook Handler (Stripe + PayPal)
3
+ // ============================================================
4
+
5
+ import { Request, Response } from 'express';
6
+ import { createLogger } from '../utils/logger';
7
+ import { stripeConfig, paypalConfig } from '../config';
8
+ import {
9
+ WebhookEvent,
10
+ ProcessedWebhookResult,
11
+ PaymentProvider,
12
+ Dispute,
13
+ DisputeStatus,
14
+ NotificationEvent,
15
+ } from '../types';
16
+ import { verifySignature } from '../evidence/encryption';
17
+
18
+ const log = createLogger('WebhookHandler');
19
+
20
+ // ────────────────────────────────────────────────────────────
21
+ // EVENT PROCESSING CALLBACK TYPE
22
+ // ────────────────────────────────────────────────────────────
23
+
24
+ export type DisputeCallback = (dispute: Dispute) => Promise<void>;
25
+ export type DisputeResolvedCallback = (
26
+ disputeId: string,
27
+ status: DisputeStatus,
28
+ provider: PaymentProvider
29
+ ) => Promise<void>;
30
+
31
+ // ────────────────────────────────────────────────────────────
32
+ // WEBHOOK HANDLER
33
+ // ────────────────────────────────────────────────────────────
34
+
35
+ export class WebhookHandler {
36
+ private disputeCallbacks: DisputeCallback[] = [];
37
+ private resolvedCallbacks: DisputeResolvedCallback[] = [];
38
+ private processedEventIds = new Set<string>();
39
+
40
+ // ──────────────────────────────────────────
41
+ // CALLBACK REGISTRATION
42
+ // ──────────────────────────────────────────
43
+
44
+ onDisputeDetected(cb: DisputeCallback): void {
45
+ this.disputeCallbacks.push(cb);
46
+ }
47
+
48
+ onDisputeResolved(cb: DisputeResolvedCallback): void {
49
+ this.resolvedCallbacks.push(cb);
50
+ }
51
+
52
+ // ──────────────────────────────────────────
53
+ // STRIPE WEBHOOK HANDLER (Express middleware)
54
+ // ──────────────────────────────────────────
55
+
56
+ handleStripe(): (req: Request, res: Response) => Promise<void> {
57
+ return async (req: Request, res: Response) => {
58
+ const signature = req.headers['stripe-signature'] as string;
59
+ const rawBody = (req as Request & { rawBody?: Buffer }).rawBody ?? req.body;
60
+
61
+ if (!signature) {
62
+ res.status(400).json({ error: 'Missing stripe-signature header' });
63
+ return;
64
+ }
65
+
66
+ let event: import('stripe').Stripe.Event;
67
+ try {
68
+ const { StripeIntegration } = await import('./stripe');
69
+ const stripe = new StripeIntegration();
70
+ event = stripe.verifyWebhookSignature(rawBody, signature);
71
+ } catch (err) {
72
+ log.warn('Stripe webhook signature verification failed', {
73
+ error: err instanceof Error ? err.message : String(err),
74
+ });
75
+ res.status(400).json({ error: 'Invalid signature' });
76
+ return;
77
+ }
78
+
79
+ // Idempotency check
80
+ if (this.processedEventIds.has(event.id)) {
81
+ log.debug(`Duplicate event ignored: ${event.id}`);
82
+ res.status(200).json({ received: true, duplicate: true });
83
+ return;
84
+ }
85
+ this.processedEventIds.add(event.id);
86
+ if (this.processedEventIds.size > 10000) {
87
+ // Keep memory bounded
88
+ const oldest = this.processedEventIds.values().next().value;
89
+ if (oldest) { this.processedEventIds.delete(oldest); }
90
+ }
91
+
92
+ const result = await this._processStripeEvent(event);
93
+ res.status(200).json(result);
94
+ };
95
+ }
96
+
97
+ // ──────────────────────────────────────────
98
+ // PAYPAL WEBHOOK HANDLER
99
+ // ──────────────────────────────────────────
100
+
101
+ handlePayPal(): (req: Request, res: Response) => Promise<void> {
102
+ return async (req: Request, res: Response) => {
103
+ const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
104
+
105
+ // Verify PayPal signature
106
+ try {
107
+ const { PayPalIntegration } = await import('./paypal');
108
+ const paypal = new PayPalIntegration();
109
+ const valid = await paypal.verifyWebhookSignature(
110
+ req.headers as Record<string, string>,
111
+ body
112
+ );
113
+ if (!valid) {
114
+ res.status(400).json({ error: 'Invalid PayPal signature' });
115
+ return;
116
+ }
117
+ } catch (err) {
118
+ log.warn('PayPal webhook verification failed', {
119
+ error: err instanceof Error ? err.message : String(err),
120
+ });
121
+ // Allow in development
122
+ if (process.env.NODE_ENV === 'production') {
123
+ res.status(400).json({ error: 'Signature verification failed' });
124
+ return;
125
+ }
126
+ }
127
+
128
+ const result = await this._processPayPalEvent(req.body as Record<string, unknown>);
129
+ res.status(200).json(result);
130
+ };
131
+ }
132
+
133
+ // ──────────────────────────────────────────
134
+ // PROCESS STRIPE EVENT
135
+ // ──────────────────────────────────────────
136
+
137
+ private async _processStripeEvent(
138
+ event: import('stripe').Stripe.Event
139
+ ): Promise<ProcessedWebhookResult> {
140
+ log.info(`Processing Stripe event: ${event.type} (${event.id})`);
141
+
142
+ const obj = event.data.object as Record<string, unknown>;
143
+
144
+ switch (event.type) {
145
+ case 'charge.dispute.created': {
146
+ const dispute = this._parseStripeDispute(obj, DisputeStatus.NEEDS_RESPONSE);
147
+ await this._fireDisputeCallbacks(dispute);
148
+ return this._result(event.id, event.type, 'dispute_detected');
149
+ }
150
+
151
+ case 'charge.dispute.updated': {
152
+ const dispute = this._parseStripeDispute(obj);
153
+ await this._fireDisputeCallbacks(dispute);
154
+ return this._result(event.id, event.type, 'dispute_updated');
155
+ }
156
+
157
+ case 'charge.dispute.closed': {
158
+ const disputeId = obj['id'] as string;
159
+ const status = obj['status'] as string;
160
+ const resolved = this._mapStripeStatus(status);
161
+ await this._fireResolvedCallbacks(disputeId, resolved, PaymentProvider.STRIPE);
162
+
163
+ if (resolved === DisputeStatus.WON) {
164
+ log.info(`🏆 Dispute WON: ${disputeId}`);
165
+ } else if (resolved === DisputeStatus.LOST) {
166
+ log.warn(`❌ Dispute LOST: ${disputeId}`);
167
+ }
168
+
169
+ return this._result(event.id, event.type, `dispute_${status}`);
170
+ }
171
+
172
+ case 'charge.dispute.funds_withdrawn':
173
+ log.warn(`💸 Funds withdrawn for dispute: ${obj['id']}`);
174
+ return this._result(event.id, event.type, 'funds_withdrawn');
175
+
176
+ case 'charge.dispute.funds_reinstated':
177
+ log.info(`💰 Funds reinstated for dispute: ${obj['id']}`);
178
+ return this._result(event.id, event.type, 'funds_reinstated');
179
+
180
+ default:
181
+ log.debug(`Unhandled Stripe event: ${event.type}`);
182
+ return this._result(event.id, event.type, 'unhandled');
183
+ }
184
+ }
185
+
186
+ // ──────────────────────────────────────────
187
+ // PROCESS PAYPAL EVENT
188
+ // ──────────────────────────────────────────
189
+
190
+ private async _processPayPalEvent(
191
+ body: Record<string, unknown>
192
+ ): Promise<ProcessedWebhookResult> {
193
+ const eventType = String(body['event_type'] ?? '');
194
+ const eventId = String(body['id'] ?? Date.now());
195
+ const resource = (body['resource'] as Record<string, unknown>) ?? {};
196
+
197
+ log.info(`Processing PayPal event: ${eventType} (${eventId})`);
198
+
199
+ switch (eventType) {
200
+ case 'CUSTOMER.DISPUTE.CREATED': {
201
+ const dispute = this._parsePayPalDispute(resource, DisputeStatus.NEEDS_RESPONSE);
202
+ await this._fireDisputeCallbacks(dispute);
203
+ return this._result(eventId, eventType, 'dispute_detected');
204
+ }
205
+
206
+ case 'CUSTOMER.DISPUTE.UPDATED': {
207
+ const dispute = this._parsePayPalDispute(resource);
208
+ await this._fireDisputeCallbacks(dispute);
209
+ return this._result(eventId, eventType, 'dispute_updated');
210
+ }
211
+
212
+ case 'CUSTOMER.DISPUTE.RESOLVED': {
213
+ const disputeId = String(resource['dispute_id'] ?? '');
214
+ const status = String(resource['status'] ?? 'RESOLVED');
215
+ await this._fireResolvedCallbacks(
216
+ disputeId,
217
+ DisputeStatus.CHARGE_REFUNDED,
218
+ PaymentProvider.PAYPAL
219
+ );
220
+ return this._result(eventId, eventType, `dispute_${status.toLowerCase()}`);
221
+ }
222
+
223
+ default:
224
+ return this._result(eventId, eventType, 'unhandled');
225
+ }
226
+ }
227
+
228
+ // ──────────────────────────────────────────
229
+ // FIRE CALLBACKS
230
+ // ──────────────────────────────────────────
231
+
232
+ private async _fireDisputeCallbacks(dispute: Dispute): Promise<void> {
233
+ for (const cb of this.disputeCallbacks) {
234
+ try { await cb(dispute); }
235
+ catch (err) {
236
+ log.error('Dispute callback error', {
237
+ error: err instanceof Error ? err.message : String(err),
238
+ });
239
+ }
240
+ }
241
+ }
242
+
243
+ private async _fireResolvedCallbacks(
244
+ disputeId: string,
245
+ status: DisputeStatus,
246
+ provider: PaymentProvider
247
+ ): Promise<void> {
248
+ for (const cb of this.resolvedCallbacks) {
249
+ try { await cb(disputeId, status, provider); }
250
+ catch (err) {
251
+ log.error('Resolved callback error', {
252
+ error: err instanceof Error ? err.message : String(err),
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ // ──────────────────────────────────────────
259
+ // PARSERS
260
+ // ──────────────────────────────────────────
261
+
262
+ private _parseStripeDispute(
263
+ obj: Record<string, unknown>,
264
+ forceStatus?: DisputeStatus
265
+ ): Dispute {
266
+ return {
267
+ id: String(obj['id'] ?? ''),
268
+ orderId: ((obj['metadata'] as Record<string, string>)?.['order_id']) ?? undefined,
269
+ amount: Number(obj['amount'] ?? 0),
270
+ currency: String(obj['currency'] ?? 'usd'),
271
+ reason: String(obj['reason'] ?? 'general') as Dispute['reason'],
272
+ status: forceStatus ?? this._mapStripeStatus(String(obj['status'] ?? 'needs_response')),
273
+ provider: PaymentProvider.STRIPE,
274
+ evidenceDueBy: obj['evidence_due_by']
275
+ ? new Date(Number(obj['evidence_due_by']) * 1000).toISOString()
276
+ : undefined,
277
+ createdAt: new Date(Number(obj['created'] ?? Date.now() / 1000) * 1000).toISOString(),
278
+ metadata: (obj['metadata'] as Record<string, unknown>) ?? {},
279
+ };
280
+ }
281
+
282
+ private _parsePayPalDispute(
283
+ resource: Record<string, unknown>,
284
+ forceStatus?: DisputeStatus
285
+ ): Dispute {
286
+ const amt = (resource['dispute_amount'] as Record<string, string> | undefined);
287
+ return {
288
+ id: String(resource['dispute_id'] ?? ''),
289
+ amount: Math.round(parseFloat(amt?.['value'] ?? '0') * 100),
290
+ currency: amt?.['currency_code'] ?? 'USD',
291
+ reason: String(resource['reason'] ?? 'OTHER') as Dispute['reason'],
292
+ status: forceStatus ?? DisputeStatus.NEEDS_RESPONSE,
293
+ provider: PaymentProvider.PAYPAL,
294
+ createdAt: String(resource['create_time'] ?? new Date().toISOString()),
295
+ metadata: { raw: resource },
296
+ };
297
+ }
298
+
299
+ // ──────────────────────────────────────────
300
+ // HELPERS
301
+ // ──────────────────────────────────────────
302
+
303
+ private _mapStripeStatus(status: string): DisputeStatus {
304
+ const map: Record<string, DisputeStatus> = {
305
+ needs_response: DisputeStatus.NEEDS_RESPONSE,
306
+ under_review: DisputeStatus.UNDER_REVIEW,
307
+ charge_refunded: DisputeStatus.CHARGE_REFUNDED,
308
+ won: DisputeStatus.WON,
309
+ lost: DisputeStatus.LOST,
310
+ warning_needs_response: DisputeStatus.WARNING_NEEDS_RESPONSE,
311
+ warning_under_review: DisputeStatus.WARNING_UNDER_REVIEW,
312
+ warning_closed: DisputeStatus.WARNING_CLOSED,
313
+ };
314
+ return map[status] ?? DisputeStatus.NEEDS_RESPONSE;
315
+ }
316
+
317
+ private _result(
318
+ eventId: string,
319
+ type: string,
320
+ action: string
321
+ ): ProcessedWebhookResult {
322
+ return {
323
+ eventId,
324
+ type,
325
+ processed: true,
326
+ action,
327
+ processedAt: new Date().toISOString(),
328
+ };
329
+ }
330
+ }
331
+
332
+ export const webhookHandler = new WebhookHandler();