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.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/docs/api.md +278 -0
- package/docs/architecture.md +281 -0
- package/docs/configuration.md +292 -0
- package/docs/getting-started.md +155 -0
- package/examples/advancedConfig.ts +123 -0
- package/examples/basicUsage.ts +98 -0
- package/examples/stripeIntegration.ts +106 -0
- package/package.json +181 -0
- package/src/ai/fraudDetection.ts +261 -0
- package/src/ai/patternRecognition.ts +218 -0
- package/src/analytics/dashboard.ts +195 -0
- package/src/analytics/metrics.ts +175 -0
- package/src/analytics/predictions.ts +135 -0
- package/src/analytics/reports.ts +221 -0
- package/src/api/controllers.ts +339 -0
- package/src/api/middleware.ts +172 -0
- package/src/api/routes.ts +141 -0
- package/src/config.ts +231 -0
- package/src/core/chargebackGuard.ts +616 -0
- package/src/core/eventEmitter.ts +118 -0
- package/src/core/lifecycle.ts +215 -0
- package/src/database/schema.ts +392 -0
- package/src/dispute/analyzer.ts +317 -0
- package/src/dispute/bankIntegration.ts +274 -0
- package/src/dispute/detector.ts +239 -0
- package/src/dispute/responseEngine.ts +440 -0
- package/src/evidence/collector.ts +426 -0
- package/src/evidence/encryption.ts +168 -0
- package/src/evidence/storage.ts +197 -0
- package/src/evidence/validator.ts +184 -0
- package/src/index.ts +43 -0
- package/src/integrations/paypal.ts +258 -0
- package/src/integrations/stripe.ts +280 -0
- package/src/integrations/webhook.ts +332 -0
- package/src/notifications/email.ts +161 -0
- package/src/notifications/inApp.ts +319 -0
- package/src/notifications/sms.ts +58 -0
- package/src/security/auth.ts +153 -0
- package/src/security/rateLimit.ts +77 -0
- package/src/security/validation.ts +166 -0
- package/src/server.ts +122 -0
- package/src/types/index.ts +790 -0
- package/src/utils/formatters.ts +72 -0
- package/src/utils/helpers.ts +193 -0
- package/src/utils/logger.ts +88 -0
- 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
|
+
}
|