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,239 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Dispute Detector
3
+ // Parses incoming webhook events and identifies disputes
4
+ // ============================================================
5
+
6
+ import { createLogger } from '../utils/logger';
7
+ import {
8
+ Dispute,
9
+ DisputeReason,
10
+ DisputeStatus,
11
+ PaymentProvider,
12
+ WebhookEvent,
13
+ } from '../types';
14
+
15
+ const log = createLogger('DisputeDetector');
16
+
17
+ // ────────────────────────────────────────────────────────────
18
+ // STRIPE REASON MAP
19
+ // ────────────────────────────────────────────────────────────
20
+
21
+ const STRIPE_REASON_MAP: Record<string, DisputeReason> = {
22
+ 'product_not_received': DisputeReason.PRODUCT_NOT_RECEIVED,
23
+ 'product_unacceptable': DisputeReason.PRODUCT_UNACCEPTABLE,
24
+ 'fraudulent': DisputeReason.FRAUDULENT,
25
+ 'unrecognized': DisputeReason.UNAUTHORIZED_TRANSACTION,
26
+ 'duplicate': DisputeReason.DUPLICATE_TRANSACTION,
27
+ 'credit_not_processed': DisputeReason.CREDIT_NOT_PROCESSED,
28
+ 'subscription_canceled': DisputeReason.SUBSCRIPTION_CANCELLED,
29
+ 'general': DisputeReason.GENERAL,
30
+ };
31
+
32
+ const STRIPE_STATUS_MAP: Record<string, DisputeStatus> = {
33
+ 'needs_response': DisputeStatus.NEEDS_RESPONSE,
34
+ 'under_review': DisputeStatus.UNDER_REVIEW,
35
+ 'charge_refunded': DisputeStatus.CHARGE_REFUNDED,
36
+ 'won': DisputeStatus.WON,
37
+ 'lost': DisputeStatus.LOST,
38
+ 'warning_needs_response': DisputeStatus.WARNING_NEEDS_RESPONSE,
39
+ 'warning_under_review': DisputeStatus.WARNING_UNDER_REVIEW,
40
+ 'warning_closed': DisputeStatus.WARNING_CLOSED,
41
+ };
42
+
43
+ // ────────────────────────────────────────────────────────────
44
+ // PAYPAL REASON MAP
45
+ // ────────────────────────────────────────────────────────────
46
+
47
+ const PAYPAL_REASON_MAP: Record<string, DisputeReason> = {
48
+ 'MERCHANDISE_OR_SERVICE_NOT_RECEIVED': DisputeReason.PRODUCT_NOT_RECEIVED,
49
+ 'MERCHANDISE_OR_SERVICE_NOT_AS_DESCRIBED': DisputeReason.PRODUCT_UNACCEPTABLE,
50
+ 'UNAUTHORISED': DisputeReason.UNAUTHORIZED_TRANSACTION,
51
+ 'CREDIT_NOT_PROCESSED': DisputeReason.CREDIT_NOT_PROCESSED,
52
+ 'DUPLICATE_TRANSACTION': DisputeReason.DUPLICATE_TRANSACTION,
53
+ 'INCORRECT_AMOUNT': DisputeReason.GENERAL,
54
+ 'PAYMENT_BY_OTHER_MEANS': DisputeReason.GENERAL,
55
+ 'CANCELED_RECURRING_BILLING': DisputeReason.SUBSCRIPTION_CANCELLED,
56
+ 'PROBLEM_WITH_REMITTANCE': DisputeReason.GENERAL,
57
+ 'OTHER': DisputeReason.GENERAL,
58
+ };
59
+
60
+ // ────────────────────────────────────────────────────────────
61
+ // DISPUTE DETECTOR
62
+ // ────────────────────────────────────────────────────────────
63
+
64
+ export class DisputeDetector {
65
+
66
+ // ──────────────────────────────────────────
67
+ // DETECT FROM WEBHOOK EVENT
68
+ // ──────────────────────────────────────────
69
+
70
+ detect(event: WebhookEvent): Dispute | null {
71
+ log.debug(`Detecting dispute from event: ${event.type}`);
72
+
73
+ switch (event.provider) {
74
+ case PaymentProvider.STRIPE:
75
+ return this._detectStripe(event);
76
+ case PaymentProvider.PAYPAL:
77
+ return this._detectPayPal(event);
78
+ default:
79
+ log.warn(`Unsupported provider for dispute detection: ${event.provider}`);
80
+ return null;
81
+ }
82
+ }
83
+
84
+ // ──────────────────────────────────────────
85
+ // STRIPE DETECTION
86
+ // ──────────────────────────────────────────
87
+
88
+ private _detectStripe(event: WebhookEvent): Dispute | null {
89
+ const disputeEventTypes = [
90
+ 'charge.dispute.created',
91
+ 'charge.dispute.updated',
92
+ 'charge.dispute.closed',
93
+ 'charge.dispute.funds_withdrawn',
94
+ 'charge.dispute.funds_reinstated',
95
+ ];
96
+
97
+ if (!disputeEventTypes.includes(event.type)) {
98
+ return null;
99
+ }
100
+
101
+ const obj = event.data.object as Record<string, unknown>;
102
+
103
+ const rawReason = (obj['reason'] as string) ?? 'general';
104
+ const rawStatus = (obj['status'] as string) ?? 'needs_response';
105
+ const orderId = (obj['metadata'] as Record<string, string>)?.['order_id']
106
+ ?? (obj['metadata'] as Record<string, string>)?.['orderId']
107
+ ?? undefined;
108
+
109
+ const dispute: Dispute = {
110
+ id: obj['id'] as string,
111
+ orderId,
112
+ amount: (obj['amount'] as number) ?? 0,
113
+ currency: (obj['currency'] as string) ?? 'usd',
114
+ reason: STRIPE_REASON_MAP[rawReason] ?? DisputeReason.GENERAL,
115
+ status: STRIPE_STATUS_MAP[rawStatus] ?? DisputeStatus.NEEDS_RESPONSE,
116
+ provider: PaymentProvider.STRIPE,
117
+ evidenceDueBy: obj['evidence_due_by']
118
+ ? new Date((obj['evidence_due_by'] as number) * 1000).toISOString()
119
+ : undefined,
120
+ createdAt: new Date((obj['created'] as number) * 1000).toISOString(),
121
+ metadata: obj['metadata'] as Record<string, unknown>,
122
+ evidence: obj['evidence'] as Record<string, unknown>,
123
+ };
124
+
125
+ log.info(`Stripe dispute detected: ${dispute.id}`, {
126
+ reason: dispute.reason,
127
+ status: dispute.status,
128
+ amount: dispute.amount,
129
+ });
130
+
131
+ return dispute;
132
+ }
133
+
134
+ // ──────────────────────────────────────────
135
+ // PAYPAL DETECTION
136
+ // ──────────────────────────────────────────
137
+
138
+ private _detectPayPal(event: WebhookEvent): Dispute | null {
139
+ const disputeEventTypes = [
140
+ 'CUSTOMER.DISPUTE.CREATED',
141
+ 'CUSTOMER.DISPUTE.UPDATED',
142
+ 'CUSTOMER.DISPUTE.RESOLVED',
143
+ ];
144
+
145
+ if (!disputeEventTypes.includes(event.type)) {
146
+ return null;
147
+ }
148
+
149
+ const resource = event.data.object as Record<string, unknown>;
150
+ const disputeInfo = (resource['dispute'] as Record<string, unknown>) ?? resource;
151
+
152
+ const rawReason = (disputeInfo['reason'] as string) ?? 'OTHER';
153
+ const rawStatus = (disputeInfo['status'] as string) ?? 'OPEN';
154
+
155
+ const dispute: Dispute = {
156
+ id: disputeInfo['dispute_id'] as string ?? event.id,
157
+ orderId: this._extractPayPalOrderId(disputeInfo),
158
+ amount: this._extractPayPalAmount(disputeInfo),
159
+ currency: 'USD',
160
+ reason: PAYPAL_REASON_MAP[rawReason] ?? DisputeReason.GENERAL,
161
+ status: this._mapPayPalStatus(rawStatus),
162
+ provider: PaymentProvider.PAYPAL,
163
+ createdAt: (disputeInfo['create_time'] as string) ?? new Date().toISOString(),
164
+ metadata: { raw: disputeInfo },
165
+ };
166
+
167
+ log.info(`PayPal dispute detected: ${dispute.id}`, {
168
+ reason: dispute.reason,
169
+ amount: dispute.amount,
170
+ });
171
+
172
+ return dispute;
173
+ }
174
+
175
+ // ──────────────────────────────────────────
176
+ // HELPERS
177
+ // ──────────────────────────────────────────
178
+
179
+ private _extractPayPalOrderId(resource: Record<string, unknown>): string | undefined {
180
+ const transactions = resource['disputed_transactions'] as Array<Record<string, unknown>>;
181
+ if (transactions?.length) {
182
+ return transactions[0]['seller_transaction_id'] as string;
183
+ }
184
+ return undefined;
185
+ }
186
+
187
+ private _extractPayPalAmount(resource: Record<string, unknown>): number {
188
+ const amount = (resource['dispute_amount'] as Record<string, string>)?.['value'];
189
+ return amount ? parseFloat(amount) * 100 : 0; // convert to cents
190
+ }
191
+
192
+ private _mapPayPalStatus(status: string): DisputeStatus {
193
+ const map: Record<string, DisputeStatus> = {
194
+ 'OPEN': DisputeStatus.NEEDS_RESPONSE,
195
+ 'WAITING_FOR_BUYER_RESPONSE': DisputeStatus.UNDER_REVIEW,
196
+ 'WAITING_FOR_SELLER_RESPONSE': DisputeStatus.NEEDS_RESPONSE,
197
+ 'UNDER_REVIEW': DisputeStatus.UNDER_REVIEW,
198
+ 'RESOLVED': DisputeStatus.CHARGE_REFUNDED,
199
+ 'OTHER': DisputeStatus.NEEDS_RESPONSE,
200
+ };
201
+ return map[status] ?? DisputeStatus.NEEDS_RESPONSE;
202
+ }
203
+
204
+ // ──────────────────────────────────────────
205
+ // IS DISPUTE EVENT
206
+ // ──────────────────────────────────────────
207
+
208
+ isDisputeEvent(event: WebhookEvent): boolean {
209
+ return this.detect(event) !== null;
210
+ }
211
+
212
+ // ──────────────────────────────────────────
213
+ // IS ACTIONABLE
214
+ // ──────────────────────────────────────────
215
+
216
+ isActionable(dispute: Dispute): boolean {
217
+ return (
218
+ dispute.status === DisputeStatus.NEEDS_RESPONSE ||
219
+ dispute.status === DisputeStatus.WARNING_NEEDS_RESPONSE
220
+ );
221
+ }
222
+
223
+ // ──────────────────────────────────────────
224
+ // IS DEADLINE CLOSE
225
+ // ──────────────────────────────────────────
226
+
227
+ isDeadlineClose(dispute: Dispute, hoursThreshold = 48): boolean {
228
+ if (!dispute.evidenceDueBy) { return false; }
229
+ const dueMs = new Date(dispute.evidenceDueBy).getTime();
230
+ const remaining = dueMs - Date.now();
231
+ return remaining > 0 && remaining < hoursThreshold * 3600000;
232
+ }
233
+
234
+ getRemainingHours(dispute: Dispute): number | null {
235
+ if (!dispute.evidenceDueBy) { return null; }
236
+ const remaining = new Date(dispute.evidenceDueBy).getTime() - Date.now();
237
+ return Math.max(0, Math.floor(remaining / 3600000));
238
+ }
239
+ }
@@ -0,0 +1,440 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Response Engine
3
+ // Builds the bank submission package for each dispute type
4
+ // ============================================================
5
+
6
+ import { createLogger } from '../utils/logger';
7
+ import {
8
+ Dispute,
9
+ DisputeAnalysis,
10
+ Evidence,
11
+ DisputeReply,
12
+ EvidenceItem,
13
+ ReplySummary,
14
+ BankReplyFormat,
15
+ Attachment,
16
+ DisputeReason,
17
+ EvidenceType,
18
+ ConfidenceLevel,
19
+ } from '../types';
20
+
21
+ const log = createLogger('ResponseEngine');
22
+
23
+ // ────────────────────────────────────────────────────────────
24
+ // RESPONSE ENGINE
25
+ // ────────────────────────────────────────────────────────────
26
+
27
+ export class ResponseEngine {
28
+
29
+ async generateReply(ctx: {
30
+ dispute: Dispute;
31
+ evidence: Evidence | null;
32
+ analysis: DisputeAnalysis;
33
+ }): Promise<DisputeReply> {
34
+ const { dispute, evidence, analysis } = ctx;
35
+ log.debug(`Generating reply for dispute: ${dispute.id}`, { reason: dispute.reason });
36
+
37
+ const evidenceItems = this._buildEvidenceItems(dispute, evidence);
38
+ const summary = this._buildSummary(evidence, analysis);
39
+ const caseArgument = this._buildCaseArgument(dispute, evidence, analysis);
40
+ const attachments = this._prepareAttachments(evidenceItems);
41
+
42
+ const reply: DisputeReply = {
43
+ disputeId: dispute.id,
44
+ orderId: dispute.orderId,
45
+ generatedAt: new Date().toISOString(),
46
+ evidenceItems,
47
+ summary,
48
+ caseArgument,
49
+ attachments,
50
+ bankFormat: this._formatForBank(dispute, evidenceItems, caseArgument, reply as DisputeReply),
51
+ };
52
+
53
+ log.info(`Reply generated: ${dispute.id}`, {
54
+ evidenceItems: evidenceItems.length,
55
+ confidenceLevel: summary.confidenceLevel,
56
+ });
57
+
58
+ return reply;
59
+ }
60
+
61
+ // ──────────────────────────────────────────
62
+ // BUILD EVIDENCE ITEMS BY DISPUTE TYPE
63
+ // ──────────────────────────────────────────
64
+
65
+ private _buildEvidenceItems(dispute: Dispute, ev: Evidence | null): EvidenceItem[] {
66
+ const items: EvidenceItem[] = [];
67
+
68
+ // Common items for all dispute types
69
+ items.push(...this._commonItems(ev));
70
+
71
+ // Type-specific items
72
+ switch (dispute.reason) {
73
+ case DisputeReason.PRODUCT_NOT_RECEIVED:
74
+ items.push(...this._deliveryProofItems(ev));
75
+ break;
76
+
77
+ case DisputeReason.UNAUTHORIZED_TRANSACTION:
78
+ case DisputeReason.FRAUDULENT:
79
+ items.push(...this._unauthorizedItems(ev));
80
+ break;
81
+
82
+ case DisputeReason.DUPLICATE_TRANSACTION:
83
+ items.push(...this._duplicateItems(ev));
84
+ break;
85
+
86
+ case DisputeReason.PRODUCT_UNACCEPTABLE:
87
+ items.push(...this._productQualityItems(ev));
88
+ break;
89
+
90
+ case DisputeReason.CREDIT_NOT_PROCESSED:
91
+ items.push(...this._creditItems(ev));
92
+ break;
93
+
94
+ case DisputeReason.SUBSCRIPTION_CANCELLED:
95
+ items.push(...this._subscriptionItems(ev));
96
+ break;
97
+
98
+ default:
99
+ items.push(...this._generalItems(ev));
100
+ break;
101
+ }
102
+
103
+ // Sort by weight descending
104
+ return items.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0));
105
+ }
106
+
107
+ // ──────────────────────────────────────────
108
+ // COMMON ITEMS (always included)
109
+ // ──────────────────────────────────────────
110
+
111
+ private _commonItems(ev: Evidence | null): EvidenceItem[] {
112
+ const items: EvidenceItem[] = [];
113
+
114
+ if (!ev) { return items; }
115
+
116
+ // Transaction log
117
+ items.push({
118
+ type: EvidenceType.TRANSACTION_LOG,
119
+ description: 'Detailed transaction record showing the charge is legitimate',
120
+ weight: 0.5,
121
+ data: {
122
+ orderId: ev.orderId,
123
+ amount: ev.data?.transactionData?.amount,
124
+ currency: ev.data?.transactionData?.currency,
125
+ paymentMethod: ev.data?.transactionData?.paymentMethod,
126
+ cardLastFour: ev.data?.transactionData?.cardLastFour,
127
+ timestamp: ev.data?.transactionData?.timestamp,
128
+ transactionId: ev.data?.transactionData?.transactionId,
129
+ },
130
+ });
131
+
132
+ // Customer communication (email)
133
+ const emailConf = ev.data?.emailConfirmation;
134
+ if (emailConf?.sentTo) {
135
+ items.push({
136
+ type: EvidenceType.EMAIL_CONFIRMATION,
137
+ description: 'Email order confirmation was sent to customer email',
138
+ weight: 0.4,
139
+ data: {
140
+ sentTo: emailConf.sentTo,
141
+ sentAt: emailConf.sentAt,
142
+ opened: emailConf.opened,
143
+ openedAt: emailConf.openedAt,
144
+ clickedLinks: emailConf.clickedLinks,
145
+ },
146
+ });
147
+ }
148
+
149
+ return items;
150
+ }
151
+
152
+ // ──────────────────────────────────────────
153
+ // DELIVERY PROOF ITEMS
154
+ // ──────────────────────────────────────────
155
+
156
+ private _deliveryProofItems(ev: Evidence | null): EvidenceItem[] {
157
+ const items: EvidenceItem[] = [];
158
+ const shipping = ev?.data?.shippingData;
159
+
160
+ items.push({
161
+ type: EvidenceType.DELIVERY_PROOF,
162
+ description: 'Shipping documentation and delivery confirmation',
163
+ weight: 0.95,
164
+ data: {
165
+ trackingNumber: shipping?.trackingNumber ?? 'Not available',
166
+ carrier: (shipping as Record<string, unknown>)?.['carrier'] ?? 'N/A',
167
+ shippedAt: (shipping as Record<string, unknown>)?.['shippingDate'],
168
+ deliveredAt: shipping?.deliveryDate,
169
+ signature: shipping?.signature ?? false,
170
+ gpsCoordinates: shipping?.gpsCoordinates,
171
+ shippingAddress: shipping?.address,
172
+ },
173
+ });
174
+
175
+ return items;
176
+ }
177
+
178
+ // ──────────────────────────────────────────
179
+ // UNAUTHORIZED ITEMS
180
+ // ──────────────────────────────────────────
181
+
182
+ private _unauthorizedItems(ev: Evidence | null): EvidenceItem[] {
183
+ const items: EvidenceItem[] = [];
184
+
185
+ // Device fingerprint
186
+ items.push({
187
+ type: EvidenceType.DEVICE_FINGERPRINT,
188
+ description: 'Device fingerprint and IP consistency proving authorized access',
189
+ weight: 0.90,
190
+ data: {
191
+ ipAddress: ev?.data?.customerData?.ipAddress,
192
+ userAgent: ev?.data?.customerData?.userAgent,
193
+ deviceId: ev?.data?.customerData?.deviceId,
194
+ fingerprint: ev?.data?.deviceFingerprint?.fingerprint,
195
+ screenResolution: ev?.data?.deviceFingerprint?.screenResolution,
196
+ timezone: ev?.data?.deviceFingerprint?.timezone,
197
+ language: ev?.data?.deviceFingerprint?.language,
198
+ },
199
+ });
200
+
201
+ // Behavior analysis
202
+ const interaction = ev?.data?.interactionData;
203
+ items.push({
204
+ type: EvidenceType.BEHAVIOR_ANALYSIS,
205
+ description: 'User behavior analysis showing natural browsing and checkout patterns',
206
+ weight: 0.80,
207
+ data: {
208
+ sessionDuration: interaction?.sessionDuration,
209
+ pageViews: interaction?.pageViews?.length,
210
+ timeOnCheckout: interaction?.timeOnCheckout,
211
+ formInteractions: interaction?.formInteractions?.length,
212
+ scrollDepth: interaction?.scrollDepth,
213
+ },
214
+ });
215
+
216
+ // Customer history
217
+ const history = ev?.data?.customerHistory;
218
+ items.push({
219
+ type: EvidenceType.CUSTOMER_HISTORY,
220
+ description: 'Customer account history and reputation score',
221
+ weight: 0.75,
222
+ data: {
223
+ accountCreatedAt: history?.accountCreatedAt,
224
+ accountAgeDays: history?.accountAge,
225
+ previousPurchases: history?.previousPurchases,
226
+ previousDisputes: history?.previousDisputes,
227
+ riskScore: history?.riskScore,
228
+ trustScore: history?.trustScore,
229
+ },
230
+ });
231
+
232
+ // Geolocation
233
+ const geo = ev?.data?.geolocation;
234
+ if (geo) {
235
+ items.push({
236
+ type: EvidenceType.GEOLOCATION,
237
+ description: 'IP geolocation matches customer registered location',
238
+ weight: 0.65,
239
+ data: {
240
+ ip: geo.ip,
241
+ country: geo.country,
242
+ city: geo.city,
243
+ timezone: geo.timezone,
244
+ isp: geo.isp,
245
+ },
246
+ });
247
+ }
248
+
249
+ return items;
250
+ }
251
+
252
+ // ──────────────────────────────────────────
253
+ // DUPLICATE ITEMS
254
+ // ──────────────────────────────────────────
255
+
256
+ private _duplicateItems(ev: Evidence | null): EvidenceItem[] {
257
+ return [{
258
+ type: EvidenceType.PAYMENT_LOG,
259
+ description: 'Payment log proving this was a single unique transaction',
260
+ weight: 0.95,
261
+ data: {
262
+ attempts: ev?.data?.paymentLog?.attempts,
263
+ successful: ev?.data?.paymentLog?.successful,
264
+ failed: ev?.data?.paymentLog?.failed,
265
+ lastAttempt: ev?.data?.paymentLog?.lastAttemptAt,
266
+ conclusion: 'Only one successful charge was made for this order',
267
+ },
268
+ }];
269
+ }
270
+
271
+ // ──────────────────────────────────────────
272
+ // PRODUCT QUALITY ITEMS
273
+ // ──────────────────────────────────────────
274
+
275
+ private _productQualityItems(ev: Evidence | null): EvidenceItem[] {
276
+ return [{
277
+ type: 'product_description',
278
+ description: 'Product description and specifications at time of purchase',
279
+ weight: 0.80,
280
+ data: {
281
+ orderId: ev?.orderId,
282
+ note: 'Product was delivered as described per order confirmation',
283
+ },
284
+ }];
285
+ }
286
+
287
+ // ──────────────────────────────────────────
288
+ // CREDIT NOT PROCESSED ITEMS
289
+ // ──────────────────────────────────────────
290
+
291
+ private _creditItems(ev: Evidence | null): EvidenceItem[] {
292
+ return [{
293
+ type: 'refund_policy',
294
+ description: 'Merchant refund policy and credit processing timeline',
295
+ weight: 0.85,
296
+ data: {
297
+ policy: 'Refund policy was clearly disclosed at time of purchase',
298
+ note: 'If a refund was agreed, see attached credit documentation',
299
+ orderId: ev?.orderId,
300
+ },
301
+ }];
302
+ }
303
+
304
+ // ──────────────────────────────────────────
305
+ // SUBSCRIPTION ITEMS
306
+ // ──────────────────────────────────────────
307
+
308
+ private _subscriptionItems(ev: Evidence | null): EvidenceItem[] {
309
+ return [{
310
+ type: 'subscription_terms',
311
+ description: 'Subscription terms and cancellation policy accepted by customer',
312
+ weight: 0.85,
313
+ data: {
314
+ note: 'Customer agreed to subscription terms during signup',
315
+ cancellationPolicy: 'Cancel anytime — no active cancellation request was received',
316
+ orderId: ev?.orderId,
317
+ },
318
+ }];
319
+ }
320
+
321
+ // ──────────────────────────────────────────
322
+ // GENERAL ITEMS
323
+ // ──────────────────────────────────────────
324
+
325
+ private _generalItems(ev: Evidence | null): EvidenceItem[] {
326
+ return [{
327
+ type: 'general_rebuttal',
328
+ description: 'General rebuttal with all available supporting evidence',
329
+ weight: 0.60,
330
+ data: {
331
+ note: 'All available evidence has been submitted. The charge is valid and service/product was delivered.',
332
+ orderId: ev?.orderId,
333
+ trustScore: ev?.trustScore,
334
+ },
335
+ }];
336
+ }
337
+
338
+ // ──────────────────────────────────────────
339
+ // SUMMARY
340
+ // ──────────────────────────────────────────
341
+
342
+ private _buildSummary(ev: Evidence | null, analysis: DisputeAnalysis): ReplySummary {
343
+ const confidenceLevel = this._scoreToConfidence(analysis.confidenceScore);
344
+
345
+ return {
346
+ trustScore: ev?.trustScore ?? 0,
347
+ confidenceLevel,
348
+ recommendation: 'APPROVE_CUSTOMER_CHARGE',
349
+ keyPoints: analysis.analysisReasons,
350
+ };
351
+ }
352
+
353
+ // ──────────────────────────────────────────
354
+ // CASE ARGUMENT
355
+ // ──────────────────────────────────────────
356
+
357
+ private _buildCaseArgument(
358
+ dispute: Dispute,
359
+ ev: Evidence | null,
360
+ analysis: DisputeAnalysis
361
+ ): string {
362
+ const lines = [
363
+ '─────────────────────────────────────────────────',
364
+ 'MERCHANT REBUTTAL — CHARGEBACK DEFENSE SUBMISSION',
365
+ '─────────────────────────────────────────────────',
366
+ '',
367
+ `Dispute ID : ${dispute.id}`,
368
+ `Order ID : ${dispute.orderId ?? 'N/A'}`,
369
+ `Dispute Type : ${dispute.reason}`,
370
+ `Amount : ${(dispute.amount / 100).toFixed(2)} ${(dispute.currency ?? 'USD').toUpperCase()}`,
371
+ `Generated At : ${new Date().toISOString()}`,
372
+ '',
373
+ 'EXECUTIVE SUMMARY',
374
+ '─────────────────',
375
+ 'This chargeback is invalid. The attached evidence conclusively demonstrates',
376
+ 'that the transaction was authorized by the cardholder and the order was',
377
+ 'fulfilled per the original purchase agreement.',
378
+ '',
379
+ ];
380
+
381
+ analysis.analysisReasons.forEach((reason, i) => {
382
+ lines.push(`${i + 1}. ${reason}`);
383
+ });
384
+
385
+ lines.push('');
386
+ lines.push(`Evidence Confidence Score : ${(analysis.confidenceScore * 100).toFixed(1)}%`);
387
+ lines.push(`Fraud Probability : ${(analysis.fraudProbability * 100).toFixed(1)}%`);
388
+ lines.push('');
389
+ lines.push('Based on all submitted evidence, we respectfully request that this');
390
+ lines.push('chargeback be ruled in favor of the merchant.');
391
+ lines.push('');
392
+ lines.push('─── GENERATED BY CHARGEBACKGUARD v2.0 ───');
393
+
394
+ return lines.join('\n');
395
+ }
396
+
397
+ // ──────────────────────────────────────────
398
+ // ATTACHMENTS
399
+ // ──────────────────────────────────────────
400
+
401
+ private _prepareAttachments(items: EvidenceItem[]): Attachment[] {
402
+ return items.map((item, i) => ({
403
+ type: item.type as string,
404
+ name: `evidence_${String(i + 1).padStart(2, '0')}_${item.type}.pdf`,
405
+ mimeType: 'application/pdf',
406
+ description: item.description,
407
+ }));
408
+ }
409
+
410
+ // ──────────────────────────────────────────
411
+ // BANK FORMAT
412
+ // ──────────────────────────────────────────
413
+
414
+ private _formatForBank(
415
+ dispute: Dispute,
416
+ evidenceItems: EvidenceItem[],
417
+ caseArgument: string,
418
+ reply: DisputeReply
419
+ ): BankReplyFormat {
420
+ return {
421
+ dispute_id: dispute.id,
422
+ merchant_reference: dispute.orderId ?? '',
423
+ submitted_at: new Date().toISOString(),
424
+ evidence_items: evidenceItems,
425
+ case_argument: caseArgument,
426
+ attachment_files: reply.attachments,
427
+ };
428
+ }
429
+
430
+ // ──────────────────────────────────────────
431
+ // HELPERS
432
+ // ──────────────────────────────────────────
433
+
434
+ private _scoreToConfidence(score: number): ConfidenceLevel {
435
+ if (score >= 0.85) { return ConfidenceLevel.VERY_HIGH; }
436
+ if (score >= 0.65) { return ConfidenceLevel.HIGH; }
437
+ if (score >= 0.45) { return ConfidenceLevel.MEDIUM; }
438
+ return ConfidenceLevel.LOW;
439
+ }
440
+ }