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,392 @@
1
+ // ============================================================
2
+ // CHARGEBACK GUARD — Database Manager (Knex / SQLite / PG)
3
+ // ============================================================
4
+
5
+ import knex, { Knex } from 'knex';
6
+ import { createLogger } from '../utils/logger';
7
+ import { dbConfig } from '../config';
8
+ import {
9
+ DatabaseConfig,
10
+ OrderRecord,
11
+ EvidenceRecord,
12
+ DisputeRecord,
13
+ MerchantRecord,
14
+ DisputeStatus,
15
+ PaymentProvider,
16
+ RiskLevel,
17
+ } from '../types';
18
+
19
+ const log = createLogger('Database');
20
+
21
+ // ────────────────────────────────────────────────────────────
22
+ // DATABASE MANAGER
23
+ // ────────────────────────────────────────────────────────────
24
+
25
+ export class DatabaseManager {
26
+ private db!: Knex;
27
+ private readonly config: DatabaseConfig;
28
+
29
+ // Sub-repositories
30
+ public orders!: OrdersRepository;
31
+ public evidence!: EvidenceRepository;
32
+ public disputes!: DisputesRepository;
33
+ public merchants!: MerchantsRepository;
34
+
35
+ constructor(cfg?: Partial<DatabaseConfig>) {
36
+ this.config = { ...dbConfig, ...cfg };
37
+ }
38
+
39
+ // ──────────────────────────────────────────
40
+ // INITIALIZE
41
+ // ──────────────────────────────────────────
42
+
43
+ async initialize(): Promise<void> {
44
+ log.info('Initializing database...', { type: this.config.type });
45
+
46
+ let connection: Knex.Config['connection'];
47
+
48
+ if (this.config.type === 'sqlite') {
49
+ const dbPath = this.config.url.replace('sqlite://', '');
50
+
51
+ // Ensure directory exists
52
+ const path = require('path') as typeof import('path');
53
+ const fs = require('fs') as typeof import('fs');
54
+ const dir = path.dirname(dbPath);
55
+ if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
56
+
57
+ connection = { filename: dbPath === './data/chargeback-guard.db' ? dbPath : dbPath };
58
+ } else {
59
+ connection = this.config.url;
60
+ }
61
+
62
+ this.db = knex({
63
+ client: this.config.type === 'sqlite' ? 'better-sqlite3' : this.config.type,
64
+ connection,
65
+ useNullAsDefault: true,
66
+ pool: {
67
+ min: this.config.poolMin ?? 2,
68
+ max: this.config.poolMax ?? 10,
69
+ },
70
+ debug: this.config.debug,
71
+ });
72
+
73
+ await this._runMigrations();
74
+
75
+ // Initialize repositories
76
+ this.orders = new OrdersRepository(this.db);
77
+ this.evidence = new EvidenceRepository(this.db);
78
+ this.disputes = new DisputesRepository(this.db);
79
+ this.merchants = new MerchantsRepository(this.db);
80
+
81
+ log.info('Database initialized ✅');
82
+ }
83
+
84
+ // ──────────────────────────────────────────
85
+ // MIGRATIONS
86
+ // ──────────────────────────────────────────
87
+
88
+ private async _runMigrations(): Promise<void> {
89
+ log.debug('Running migrations...');
90
+
91
+ await this.db.schema.createTableIfNotExists('merchants', t => {
92
+ t.increments('id').primary();
93
+ t.string('merchant_id', 36).notNullable().unique();
94
+ t.string('name', 255).notNullable();
95
+ t.string('email', 255).notNullable().unique();
96
+ t.string('password_hash', 255).notNullable();
97
+ t.string('api_key', 100).notNullable().unique();
98
+ t.string('plan', 50).defaultTo('free');
99
+ t.string('webhook_url', 512).nullable();
100
+ t.text('settings').nullable();
101
+ t.boolean('is_active').defaultTo(true);
102
+ t.timestamp('created_at').defaultTo(this.db.fn.now());
103
+ t.timestamp('updated_at').defaultTo(this.db.fn.now());
104
+ });
105
+
106
+ await this.db.schema.createTableIfNotExists('orders', t => {
107
+ t.increments('id').primary();
108
+ t.string('order_id', 255).notNullable().unique();
109
+ t.string('merchant_id', 36).notNullable();
110
+ t.decimal('amount', 12, 2).notNullable();
111
+ t.string('currency', 3).notNullable().defaultTo('USD');
112
+ t.string('customer_email', 255).notNullable();
113
+ t.string('status', 50).defaultTo('active');
114
+ t.string('provider', 50).defaultTo('stripe');
115
+ t.timestamp('registered_at').defaultTo(this.db.fn.now());
116
+ t.timestamp('created_at').defaultTo(this.db.fn.now());
117
+ t.timestamp('updated_at').defaultTo(this.db.fn.now());
118
+ t.index(['merchant_id']);
119
+ t.index(['order_id']);
120
+ });
121
+
122
+ await this.db.schema.createTableIfNotExists('evidence', t => {
123
+ t.increments('id').primary();
124
+ t.string('order_id', 255).notNullable();
125
+ t.string('collection_id', 64).notNullable().unique();
126
+ t.text('evidence_json').notNullable();
127
+ t.decimal('trust_score', 5, 2).defaultTo(0);
128
+ t.string('risk_level', 20).defaultTo('medium');
129
+ t.timestamp('collected_at').defaultTo(this.db.fn.now());
130
+ t.timestamp('created_at').defaultTo(this.db.fn.now());
131
+ t.index(['order_id']);
132
+ });
133
+
134
+ await this.db.schema.createTableIfNotExists('disputes', t => {
135
+ t.increments('id').primary();
136
+ t.string('dispute_id', 255).notNullable().unique();
137
+ t.string('order_id', 255).notNullable();
138
+ t.string('merchant_id', 36).nullable();
139
+ t.decimal('amount', 12, 2).defaultTo(0);
140
+ t.string('reason', 100).defaultTo('general');
141
+ t.string('status', 50).defaultTo('needs_response');
142
+ t.string('provider', 50).defaultTo('stripe');
143
+ t.text('reply_json').nullable();
144
+ t.timestamp('reply_submitted_at').nullable();
145
+ t.timestamp('evidence_due_by').nullable();
146
+ t.timestamp('resolved_at').nullable();
147
+ t.timestamp('won_at').nullable();
148
+ t.timestamp('lost_at').nullable();
149
+ t.timestamp('created_at').defaultTo(this.db.fn.now());
150
+ t.timestamp('updated_at').defaultTo(this.db.fn.now());
151
+ t.index(['dispute_id']);
152
+ t.index(['order_id']);
153
+ t.index(['status']);
154
+ });
155
+
156
+ log.debug('Migrations complete');
157
+ }
158
+
159
+ async healthCheck(): Promise<boolean> {
160
+ await this.db.raw('SELECT 1');
161
+ return true;
162
+ }
163
+
164
+ async destroy(): Promise<void> {
165
+ await this.db.destroy();
166
+ log.info('Database connection closed');
167
+ }
168
+
169
+ getKnex(): Knex {
170
+ return this.db;
171
+ }
172
+ }
173
+
174
+ // ────────────────────────────────────────────────────────────
175
+ // ORDERS REPOSITORY
176
+ // ────────────────────────────────────────────────────────────
177
+
178
+ export class OrdersRepository {
179
+ constructor(private readonly db: Knex) {}
180
+
181
+ async create(data: {
182
+ orderId: string;
183
+ amount: number;
184
+ currency: string;
185
+ customerEmail: string;
186
+ provider: string;
187
+ status?: string;
188
+ registeredAt?: string;
189
+ merchantId?: string;
190
+ }): Promise<number> {
191
+ const [id] = await this.db('orders').insert({
192
+ order_id: data.orderId,
193
+ merchant_id: data.merchantId ?? 'default',
194
+ amount: data.amount,
195
+ currency: data.currency,
196
+ customer_email: data.customerEmail,
197
+ status: data.status ?? 'active',
198
+ provider: data.provider,
199
+ registered_at: data.registeredAt ?? new Date().toISOString(),
200
+ });
201
+ return id;
202
+ }
203
+
204
+ async findByOrderId(orderId: string): Promise<OrderRecord | null> {
205
+ const row = await this.db('orders').where('order_id', orderId).first();
206
+ return row ?? null;
207
+ }
208
+
209
+ async count(from?: Date, to?: Date): Promise<number> {
210
+ let q = this.db('orders').count('* as count');
211
+ if (from) { q = q.where('created_at', '>=', from.toISOString()); }
212
+ if (to) { q = q.where('created_at', '<=', to.toISOString()); }
213
+ const [{ count }] = await q;
214
+ return Number(count ?? 0);
215
+ }
216
+
217
+ async list(page = 1, limit = 20): Promise<OrderRecord[]> {
218
+ return this.db('orders')
219
+ .orderBy('created_at', 'desc')
220
+ .limit(limit)
221
+ .offset((page - 1) * limit);
222
+ }
223
+ }
224
+
225
+ // ────────────────────────────────────────────────────────────
226
+ // EVIDENCE REPOSITORY
227
+ // ────────────────────────────────────────────────────────────
228
+
229
+ export class EvidenceRepository {
230
+ constructor(private readonly db: Knex) {}
231
+
232
+ async create(data: {
233
+ orderId: string;
234
+ collectionId: string;
235
+ evidenceJson: string;
236
+ trustScore: number;
237
+ riskLevel: string;
238
+ collectedAt?: string;
239
+ }): Promise<void> {
240
+ await this.db('evidence').insert({
241
+ order_id: data.orderId,
242
+ collection_id: data.collectionId,
243
+ evidence_json: data.evidenceJson,
244
+ trust_score: data.trustScore,
245
+ risk_level: data.riskLevel,
246
+ collected_at: data.collectedAt ?? new Date().toISOString(),
247
+ });
248
+ }
249
+
250
+ async findByOrderId(orderId: string): Promise<EvidenceRecord | null> {
251
+ const row = await this.db('evidence')
252
+ .where('order_id', orderId)
253
+ .orderBy('created_at', 'desc')
254
+ .first();
255
+ return row ?? null;
256
+ }
257
+
258
+ async findByCollectionId(collectionId: string): Promise<EvidenceRecord | null> {
259
+ const row = await this.db('evidence')
260
+ .where('collection_id', collectionId)
261
+ .first();
262
+ return row ?? null;
263
+ }
264
+
265
+ async deleteByOrderId(orderId: string): Promise<void> {
266
+ await this.db('evidence').where('order_id', orderId).delete();
267
+ }
268
+ }
269
+
270
+ // ────────────────────────────────────────────────────────────
271
+ // DISPUTES REPOSITORY
272
+ // ────────────────────────────────────────────────────────────
273
+
274
+ export class DisputesRepository {
275
+ constructor(private readonly db: Knex) {}
276
+
277
+ async upsert(data: {
278
+ disputeId: string;
279
+ orderId: string;
280
+ amount?: number;
281
+ reason?: string;
282
+ status: string;
283
+ provider?: string;
284
+ replyJson?: string;
285
+ replySubmittedAt?: string;
286
+ evidenceDueBy?: string;
287
+ merchantId?: string;
288
+ }): Promise<void> {
289
+ const existing = await this.db('disputes')
290
+ .where('dispute_id', data.disputeId)
291
+ .first();
292
+
293
+ if (existing) {
294
+ await this.db('disputes')
295
+ .where('dispute_id', data.disputeId)
296
+ .update({
297
+ status: data.status,
298
+ reply_json: data.replyJson,
299
+ reply_submitted_at: data.replySubmittedAt,
300
+ updated_at: new Date().toISOString(),
301
+ });
302
+ } else {
303
+ await this.db('disputes').insert({
304
+ dispute_id: data.disputeId,
305
+ order_id: data.orderId,
306
+ merchant_id: data.merchantId ?? 'default',
307
+ amount: data.amount ?? 0,
308
+ reason: data.reason ?? 'general',
309
+ status: data.status,
310
+ provider: data.provider ?? 'stripe',
311
+ reply_json: data.replyJson,
312
+ reply_submitted_at: data.replySubmittedAt,
313
+ evidence_due_by: data.evidenceDueBy,
314
+ });
315
+ }
316
+ }
317
+
318
+ async count(from?: Date, to?: Date): Promise<number> {
319
+ let q = this.db('disputes').count('* as count');
320
+ if (from) { q = q.where('created_at', '>=', from.toISOString()); }
321
+ if (to) { q = q.where('created_at', '<=', to.toISOString()); }
322
+ const [{ count }] = await q;
323
+ return Number(count ?? 0);
324
+ }
325
+
326
+ async countByStatus(status: string, from?: Date, to?: Date): Promise<number> {
327
+ let q = this.db('disputes').count('* as count').where('status', status);
328
+ if (from) { q = q.where('created_at', '>=', from.toISOString()); }
329
+ if (to) { q = q.where('created_at', '<=', to.toISOString()); }
330
+ const [{ count }] = await q;
331
+ return Number(count ?? 0);
332
+ }
333
+
334
+ async sumAmountByStatus(status: string, from?: Date, to?: Date): Promise<number> {
335
+ let q = this.db('disputes').sum('amount as total').where('status', status);
336
+ if (from) { q = q.where('created_at', '>=', from.toISOString()); }
337
+ if (to) { q = q.where('created_at', '<=', to.toISOString()); }
338
+ const [{ total }] = await q;
339
+ return Number(total ?? 0);
340
+ }
341
+
342
+ async avgResolutionTime(from?: Date, to?: Date): Promise<number> {
343
+ // Simplified: return placeholder value
344
+ return 3.2; // days
345
+ }
346
+
347
+ async findByStatus(status: string): Promise<DisputeRecord[]> {
348
+ return this.db('disputes').where('status', status).orderBy('created_at', 'desc');
349
+ }
350
+
351
+ async list(page = 1, limit = 20, status?: string): Promise<DisputeRecord[]> {
352
+ let q = this.db('disputes').orderBy('created_at', 'desc').limit(limit).offset((page - 1) * limit);
353
+ if (status) { q = q.where('status', status); }
354
+ return q;
355
+ }
356
+ }
357
+
358
+ // ────────────────────────────────────────────────────────────
359
+ // MERCHANTS REPOSITORY
360
+ // ────────────────────────────────────────────────────────────
361
+
362
+ export class MerchantsRepository {
363
+ constructor(private readonly db: Knex) {}
364
+
365
+ async create(data: Omit<MerchantRecord, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> {
366
+ await this.db('merchants').insert({
367
+ merchant_id: data.merchantId,
368
+ name: data.name,
369
+ email: data.email,
370
+ api_key: data.apiKey,
371
+ plan: data.plan,
372
+ webhook_url: data.webhookUrl,
373
+ settings: data.settings,
374
+ is_active: data.isActive,
375
+ });
376
+ }
377
+
378
+ async findByEmail(email: string): Promise<MerchantRecord | null> {
379
+ const row = await this.db('merchants').where('email', email.toLowerCase()).first();
380
+ return row ?? null;
381
+ }
382
+
383
+ async findByApiKey(apiKey: string): Promise<MerchantRecord | null> {
384
+ const row = await this.db('merchants').where('api_key', apiKey).first();
385
+ return row ?? null;
386
+ }
387
+
388
+ async findById(merchantId: string): Promise<MerchantRecord | null> {
389
+ const row = await this.db('merchants').where('merchant_id', merchantId).first();
390
+ return row ?? null;
391
+ }
392
+ }