pay-lobster 1.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 (120) hide show
  1. package/README.md +401 -0
  2. package/README.md.bak +401 -0
  3. package/dist/agent.d.ts +132 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +224 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/analytics.d.ts +120 -0
  8. package/dist/analytics.d.ts.map +1 -0
  9. package/dist/analytics.js +345 -0
  10. package/dist/analytics.js.map +1 -0
  11. package/dist/approvals.d.ts +168 -0
  12. package/dist/approvals.d.ts.map +1 -0
  13. package/dist/approvals.js +406 -0
  14. package/dist/approvals.js.map +1 -0
  15. package/dist/circle-client.d.ts +152 -0
  16. package/dist/circle-client.d.ts.map +1 -0
  17. package/dist/circle-client.js +266 -0
  18. package/dist/circle-client.js.map +1 -0
  19. package/dist/commission.d.ts +191 -0
  20. package/dist/commission.d.ts.map +1 -0
  21. package/dist/commission.js +475 -0
  22. package/dist/commission.js.map +1 -0
  23. package/dist/condition-builder.d.ts +98 -0
  24. package/dist/condition-builder.d.ts.map +1 -0
  25. package/dist/condition-builder.js +193 -0
  26. package/dist/condition-builder.js.map +1 -0
  27. package/dist/contacts.d.ts +179 -0
  28. package/dist/contacts.d.ts.map +1 -0
  29. package/dist/contacts.js +445 -0
  30. package/dist/contacts.js.map +1 -0
  31. package/dist/easy.d.ts +22 -0
  32. package/dist/easy.d.ts.map +1 -0
  33. package/dist/easy.js +40 -0
  34. package/dist/easy.js.map +1 -0
  35. package/dist/erc8004/constants.d.ts +152 -0
  36. package/dist/erc8004/constants.d.ts.map +1 -0
  37. package/dist/erc8004/constants.js +114 -0
  38. package/dist/erc8004/constants.js.map +1 -0
  39. package/dist/erc8004/discovery.d.ts +84 -0
  40. package/dist/erc8004/discovery.d.ts.map +1 -0
  41. package/dist/erc8004/discovery.js +217 -0
  42. package/dist/erc8004/discovery.js.map +1 -0
  43. package/dist/erc8004/identity.d.ts +91 -0
  44. package/dist/erc8004/identity.d.ts.map +1 -0
  45. package/dist/erc8004/identity.js +250 -0
  46. package/dist/erc8004/identity.js.map +1 -0
  47. package/dist/erc8004/index.d.ts +147 -0
  48. package/dist/erc8004/index.d.ts.map +1 -0
  49. package/dist/erc8004/index.js +225 -0
  50. package/dist/erc8004/index.js.map +1 -0
  51. package/dist/erc8004/reputation.d.ts +133 -0
  52. package/dist/erc8004/reputation.d.ts.map +1 -0
  53. package/dist/erc8004/reputation.js +277 -0
  54. package/dist/erc8004/reputation.js.map +1 -0
  55. package/dist/escrow-templates.d.ts +38 -0
  56. package/dist/escrow-templates.d.ts.map +1 -0
  57. package/dist/escrow-templates.js +419 -0
  58. package/dist/escrow-templates.js.map +1 -0
  59. package/dist/escrow.d.ts +320 -0
  60. package/dist/escrow.d.ts.map +1 -0
  61. package/dist/escrow.js +854 -0
  62. package/dist/escrow.js.map +1 -0
  63. package/dist/index.d.ts +11 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +33 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/invoices.d.ts +212 -0
  68. package/dist/invoices.d.ts.map +1 -0
  69. package/dist/invoices.js +393 -0
  70. package/dist/invoices.js.map +1 -0
  71. package/dist/notifications.d.ts +141 -0
  72. package/dist/notifications.d.ts.map +1 -0
  73. package/dist/notifications.js +350 -0
  74. package/dist/notifications.js.map +1 -0
  75. package/dist/tips.d.ts +171 -0
  76. package/dist/tips.d.ts.map +1 -0
  77. package/dist/tips.js +390 -0
  78. package/dist/tips.js.map +1 -0
  79. package/dist/types.d.ts +100 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +6 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/x402-client.d.ts +127 -0
  84. package/dist/x402-client.d.ts.map +1 -0
  85. package/dist/x402-client.js +350 -0
  86. package/dist/x402-client.js.map +1 -0
  87. package/dist/x402-server.d.ts +133 -0
  88. package/dist/x402-server.d.ts.map +1 -0
  89. package/dist/x402-server.js +330 -0
  90. package/dist/x402-server.js.map +1 -0
  91. package/lib/agent.ts +273 -0
  92. package/lib/analytics.ts +474 -0
  93. package/lib/analytics.ts.bak +474 -0
  94. package/lib/approvals.ts +585 -0
  95. package/lib/approvals.ts.bak +585 -0
  96. package/lib/circle-client.ts +376 -0
  97. package/lib/circle-client.ts.bak +376 -0
  98. package/lib/commission.ts +680 -0
  99. package/lib/commission.ts.bak +680 -0
  100. package/lib/condition-builder.ts +223 -0
  101. package/lib/condition-builder.ts.bak +223 -0
  102. package/lib/contacts.ts +615 -0
  103. package/lib/contacts.ts.bak +615 -0
  104. package/lib/easy.ts +46 -0
  105. package/lib/easy.ts.bak +352 -0
  106. package/lib/erc8004/constants.ts +175 -0
  107. package/lib/erc8004/discovery.ts +299 -0
  108. package/lib/erc8004/identity.ts +327 -0
  109. package/lib/erc8004/index.ts +285 -0
  110. package/lib/erc8004/reputation.ts +368 -0
  111. package/lib/escrow-templates.ts +462 -0
  112. package/lib/escrow.ts +1216 -0
  113. package/lib/index.ts +13 -0
  114. package/lib/invoices.ts +588 -0
  115. package/lib/notifications.ts +484 -0
  116. package/lib/tips.ts +570 -0
  117. package/lib/types.ts +108 -0
  118. package/lib/x402-client.ts +471 -0
  119. package/lib/x402-server.ts +462 -0
  120. package/package.json +58 -0
@@ -0,0 +1,680 @@
1
+ /**
2
+ * Commission Splitter Module
3
+ *
4
+ * Auto-distribute USDC commissions between multiple parties:
5
+ * agents, brokers, referral partners, team members.
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+
12
+ export interface SplitRecipient {
13
+ id: string;
14
+ name: string;
15
+ role: 'listing_agent' | 'buyer_agent' | 'broker' | 'referral' | 'team_member' | 'company' | 'custom';
16
+ walletAddress: string;
17
+
18
+ // Split calculation
19
+ splitType: 'percentage' | 'fixed';
20
+ splitValue: string; // e.g., "70" for 70% or "500" for $500 fixed
21
+
22
+ // Caps and minimums
23
+ minAmount?: string;
24
+ maxAmount?: string;
25
+
26
+ // For tiered splits (e.g., broker gets more after agent hits cap)
27
+ tier?: number;
28
+ }
29
+
30
+ export interface CommissionSplit {
31
+ id: string;
32
+ name: string;
33
+ description?: string;
34
+
35
+ // Transaction details
36
+ propertyAddress?: string;
37
+ mlsNumber?: string;
38
+ closingDate?: string;
39
+ salePrice?: string;
40
+
41
+ // Commission
42
+ totalCommission: string;
43
+ chain: string;
44
+ sourceWalletId: string;
45
+
46
+ // Recipients and their splits
47
+ recipients: SplitRecipient[];
48
+
49
+ // Calculated payouts
50
+ payouts: {
51
+ recipientId: string;
52
+ amount: string;
53
+ status: 'pending' | 'processing' | 'sent' | 'failed';
54
+ txHash?: string;
55
+ sentAt?: string;
56
+ error?: string;
57
+ }[];
58
+
59
+ // Overall status
60
+ status: 'draft' | 'ready' | 'processing' | 'completed' | 'partial' | 'failed';
61
+
62
+ // Execution
63
+ executeAt?: string; // Schedule for future
64
+ executedAt?: string;
65
+
66
+ // Metadata
67
+ notes?: string;
68
+ createdAt: string;
69
+ updatedAt: string;
70
+ }
71
+
72
+ export interface SplitTemplate {
73
+ id: string;
74
+ name: string;
75
+ description?: string;
76
+
77
+ // Template recipients (without specific addresses)
78
+ recipients: {
79
+ role: SplitRecipient['role'];
80
+ splitType: SplitRecipient['splitType'];
81
+ splitValue: string;
82
+ tier?: number;
83
+ }[];
84
+
85
+ // Usage tracking
86
+ usageCount: number;
87
+ lastUsedAt?: string;
88
+
89
+ createdAt: string;
90
+ }
91
+
92
+ const DATA_DIR = process.env.USDC_DATA_DIR || './data';
93
+
94
+ /**
95
+ * Commission Splitter
96
+ */
97
+ export class CommissionSplitter {
98
+ private splitsPath: string;
99
+ private templatesPath: string;
100
+
101
+ constructor(dataDir = DATA_DIR) {
102
+ this.splitsPath = path.join(dataDir, 'commission-splits.json');
103
+ this.templatesPath = path.join(dataDir, 'split-templates.json');
104
+ }
105
+
106
+ private async loadSplits(): Promise<CommissionSplit[]> {
107
+ try {
108
+ const data = await fs.readFile(this.splitsPath, 'utf-8');
109
+ return JSON.parse(data);
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ private async saveSplits(splits: CommissionSplit[]): Promise<void> {
116
+ await fs.mkdir(path.dirname(this.splitsPath), { recursive: true });
117
+ await fs.writeFile(this.splitsPath, JSON.stringify(splits, null, 2));
118
+ }
119
+
120
+ private async loadTemplates(): Promise<SplitTemplate[]> {
121
+ try {
122
+ const data = await fs.readFile(this.templatesPath, 'utf-8');
123
+ return JSON.parse(data);
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ private async saveTemplates(templates: SplitTemplate[]): Promise<void> {
130
+ await fs.mkdir(path.dirname(this.templatesPath), { recursive: true });
131
+ await fs.writeFile(this.templatesPath, JSON.stringify(templates, null, 2));
132
+ }
133
+
134
+ // ============ Split Creation ============
135
+
136
+ /**
137
+ * Create a commission split
138
+ */
139
+ async createSplit(params: {
140
+ name: string;
141
+ totalCommission: string;
142
+ chain: string;
143
+ sourceWalletId: string;
144
+ recipients: Omit<SplitRecipient, 'id'>[];
145
+ propertyAddress?: string;
146
+ mlsNumber?: string;
147
+ closingDate?: string;
148
+ salePrice?: string;
149
+ executeAt?: string;
150
+ notes?: string;
151
+ }): Promise<CommissionSplit> {
152
+ const splits = await this.loadSplits();
153
+
154
+ // Validate splits add up correctly
155
+ const validation = this.validateSplits(params.totalCommission, params.recipients as SplitRecipient[]);
156
+ if (!validation.valid) {
157
+ throw new Error(validation.error);
158
+ }
159
+
160
+ const split: CommissionSplit = {
161
+ id: `CS-${Date.now().toString(36).toUpperCase()}`,
162
+ name: params.name,
163
+ totalCommission: params.totalCommission,
164
+ chain: params.chain,
165
+ sourceWalletId: params.sourceWalletId,
166
+ propertyAddress: params.propertyAddress,
167
+ mlsNumber: params.mlsNumber,
168
+ closingDate: params.closingDate,
169
+ salePrice: params.salePrice,
170
+ recipients: params.recipients.map(r => ({
171
+ ...r,
172
+ id: crypto.randomUUID(),
173
+ })),
174
+ payouts: [],
175
+ status: 'draft',
176
+ executeAt: params.executeAt,
177
+ notes: params.notes,
178
+ createdAt: new Date().toISOString(),
179
+ updatedAt: new Date().toISOString(),
180
+ };
181
+
182
+ // Calculate payouts
183
+ split.payouts = this.calculatePayouts(split);
184
+ split.status = 'ready';
185
+
186
+ splits.push(split);
187
+ await this.saveSplits(splits);
188
+
189
+ return split;
190
+ }
191
+
192
+ /**
193
+ * Create split from template
194
+ */
195
+ async createFromTemplate(
196
+ templateId: string,
197
+ params: {
198
+ totalCommission: string;
199
+ chain: string;
200
+ sourceWalletId: string;
201
+ recipientAddresses: { role: string; name: string; address: string }[];
202
+ propertyAddress?: string;
203
+ closingDate?: string;
204
+ salePrice?: string;
205
+ }
206
+ ): Promise<CommissionSplit> {
207
+ const templates = await this.loadTemplates();
208
+ const template = templates.find(t => t.id === templateId);
209
+
210
+ if (!template) {
211
+ throw new Error('Template not found');
212
+ }
213
+
214
+ // Map template roles to actual recipients
215
+ const recipients: Omit<SplitRecipient, 'id'>[] = template.recipients.map(tr => {
216
+ const actual = params.recipientAddresses.find(ra => ra.role === tr.role);
217
+ if (!actual) {
218
+ throw new Error(`No address provided for role: ${tr.role}`);
219
+ }
220
+ return {
221
+ name: actual.name,
222
+ role: tr.role,
223
+ walletAddress: actual.address,
224
+ splitType: tr.splitType,
225
+ splitValue: tr.splitValue,
226
+ tier: tr.tier,
227
+ };
228
+ });
229
+
230
+ // Update template usage
231
+ template.usageCount++;
232
+ template.lastUsedAt = new Date().toISOString();
233
+ await this.saveTemplates(templates);
234
+
235
+ return this.createSplit({
236
+ name: `${template.name} - ${params.propertyAddress || new Date().toLocaleDateString()}`,
237
+ totalCommission: params.totalCommission,
238
+ chain: params.chain,
239
+ sourceWalletId: params.sourceWalletId,
240
+ recipients,
241
+ propertyAddress: params.propertyAddress,
242
+ closingDate: params.closingDate,
243
+ salePrice: params.salePrice,
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Quick split for common scenarios
249
+ */
250
+ async quickSplit(params: {
251
+ totalCommission: string;
252
+ chain: string;
253
+ sourceWalletId: string;
254
+ scenario: 'listing_side' | 'buyer_side' | 'both_sides' | 'referral';
255
+ agent: { name: string; address: string; split?: string };
256
+ broker: { name: string; address: string; split?: string };
257
+ referral?: { name: string; address: string; split?: string };
258
+ propertyAddress?: string;
259
+ }): Promise<CommissionSplit> {
260
+ const recipients: Omit<SplitRecipient, 'id'>[] = [];
261
+
262
+ // Default splits based on scenario
263
+ const defaultSplits = {
264
+ listing_side: { agent: '70', broker: '30' },
265
+ buyer_side: { agent: '70', broker: '30' },
266
+ both_sides: { agent: '70', broker: '30' },
267
+ referral: { agent: '50', broker: '25', referral: '25' },
268
+ };
269
+
270
+ const splits = defaultSplits[params.scenario];
271
+
272
+ // Agent
273
+ recipients.push({
274
+ name: params.agent.name,
275
+ role: params.scenario === 'buyer_side' ? 'buyer_agent' : 'listing_agent',
276
+ walletAddress: params.agent.address,
277
+ splitType: 'percentage',
278
+ splitValue: params.agent.split || splits.agent,
279
+ });
280
+
281
+ // Broker
282
+ recipients.push({
283
+ name: params.broker.name,
284
+ role: 'broker',
285
+ walletAddress: params.broker.address,
286
+ splitType: 'percentage',
287
+ splitValue: params.broker.split || splits.broker,
288
+ });
289
+
290
+ // Referral (if applicable)
291
+ if (params.referral && params.scenario === 'referral') {
292
+ recipients.push({
293
+ name: params.referral.name,
294
+ role: 'referral',
295
+ walletAddress: params.referral.address,
296
+ splitType: 'percentage',
297
+ splitValue: params.referral.split || splits.referral || '25',
298
+ });
299
+ }
300
+
301
+ return this.createSplit({
302
+ name: `Commission - ${params.propertyAddress || 'Quick Split'}`,
303
+ totalCommission: params.totalCommission,
304
+ chain: params.chain,
305
+ sourceWalletId: params.sourceWalletId,
306
+ recipients,
307
+ propertyAddress: params.propertyAddress,
308
+ });
309
+ }
310
+
311
+ // ============ Payout Calculation ============
312
+
313
+ /**
314
+ * Calculate payouts for all recipients
315
+ */
316
+ private calculatePayouts(split: CommissionSplit): CommissionSplit['payouts'] {
317
+ const total = parseFloat(split.totalCommission);
318
+ const payouts: CommissionSplit['payouts'] = [];
319
+ let remaining = total;
320
+
321
+ // Sort by tier (lower tier = paid first)
322
+ const sortedRecipients = [...split.recipients].sort((a, b) =>
323
+ (a.tier || 0) - (b.tier || 0)
324
+ );
325
+
326
+ for (const recipient of sortedRecipients) {
327
+ let amount: number;
328
+
329
+ if (recipient.splitType === 'percentage') {
330
+ amount = total * (parseFloat(recipient.splitValue) / 100);
331
+ } else {
332
+ amount = parseFloat(recipient.splitValue);
333
+ }
334
+
335
+ // Apply min/max caps
336
+ if (recipient.minAmount && amount < parseFloat(recipient.minAmount)) {
337
+ amount = parseFloat(recipient.minAmount);
338
+ }
339
+ if (recipient.maxAmount && amount > parseFloat(recipient.maxAmount)) {
340
+ amount = parseFloat(recipient.maxAmount);
341
+ }
342
+
343
+ // Don't exceed remaining
344
+ if (amount > remaining) {
345
+ amount = remaining;
346
+ }
347
+
348
+ remaining -= amount;
349
+
350
+ payouts.push({
351
+ recipientId: recipient.id,
352
+ amount: amount.toFixed(2),
353
+ status: 'pending',
354
+ });
355
+ }
356
+
357
+ return payouts;
358
+ }
359
+
360
+ /**
361
+ * Validate splits add up correctly
362
+ */
363
+ private validateSplits(
364
+ total: string,
365
+ recipients: SplitRecipient[]
366
+ ): { valid: boolean; error?: string } {
367
+ const totalAmount = parseFloat(total);
368
+ let percentageSum = 0;
369
+ let fixedSum = 0;
370
+
371
+ for (const r of recipients) {
372
+ if (r.splitType === 'percentage') {
373
+ percentageSum += parseFloat(r.splitValue);
374
+ } else {
375
+ fixedSum += parseFloat(r.splitValue);
376
+ }
377
+ }
378
+
379
+ // Check percentages
380
+ if (percentageSum > 100) {
381
+ return { valid: false, error: `Percentages sum to ${percentageSum}%, exceeds 100%` };
382
+ }
383
+
384
+ // Check fixed amounts
385
+ const percentageAmount = totalAmount * (percentageSum / 100);
386
+ if (fixedSum + percentageAmount > totalAmount) {
387
+ return { valid: false, error: 'Fixed amounts + percentages exceed total commission' };
388
+ }
389
+
390
+ return { valid: true };
391
+ }
392
+
393
+ // ============ Execution ============
394
+
395
+ /**
396
+ * Execute a split (send all payouts)
397
+ * Returns the split with updated payout statuses
398
+ */
399
+ async execute(
400
+ splitId: string,
401
+ sendFn: (to: string, amount: string, chain: string) => Promise<{ txHash: string }>
402
+ ): Promise<CommissionSplit | null> {
403
+ const splits = await this.loadSplits();
404
+ const split = splits.find(s => s.id === splitId);
405
+
406
+ if (!split || split.status !== 'ready') {
407
+ return null;
408
+ }
409
+
410
+ split.status = 'processing';
411
+ await this.saveSplits(splits);
412
+
413
+ let successCount = 0;
414
+ let failCount = 0;
415
+
416
+ for (const payout of split.payouts) {
417
+ const recipient = split.recipients.find(r => r.id === payout.recipientId);
418
+ if (!recipient) continue;
419
+
420
+ payout.status = 'processing';
421
+
422
+ try {
423
+ const result = await sendFn(recipient.walletAddress, payout.amount, split.chain);
424
+ payout.status = 'sent';
425
+ payout.txHash = result.txHash;
426
+ payout.sentAt = new Date().toISOString();
427
+ successCount++;
428
+ } catch (err: any) {
429
+ payout.status = 'failed';
430
+ payout.error = err.message;
431
+ failCount++;
432
+ }
433
+
434
+ await this.saveSplits(splits);
435
+ }
436
+
437
+ // Update overall status
438
+ if (failCount === 0) {
439
+ split.status = 'completed';
440
+ } else if (successCount === 0) {
441
+ split.status = 'failed';
442
+ } else {
443
+ split.status = 'partial';
444
+ }
445
+
446
+ split.executedAt = new Date().toISOString();
447
+ split.updatedAt = new Date().toISOString();
448
+ await this.saveSplits(splits);
449
+
450
+ return split;
451
+ }
452
+
453
+ /**
454
+ * Retry failed payouts
455
+ */
456
+ async retryFailed(
457
+ splitId: string,
458
+ sendFn: (to: string, amount: string, chain: string) => Promise<{ txHash: string }>
459
+ ): Promise<CommissionSplit | null> {
460
+ const splits = await this.loadSplits();
461
+ const split = splits.find(s => s.id === splitId);
462
+
463
+ if (!split) return null;
464
+
465
+ const failedPayouts = split.payouts.filter(p => p.status === 'failed');
466
+
467
+ for (const payout of failedPayouts) {
468
+ const recipient = split.recipients.find(r => r.id === payout.recipientId);
469
+ if (!recipient) continue;
470
+
471
+ try {
472
+ const result = await sendFn(recipient.walletAddress, payout.amount, split.chain);
473
+ payout.status = 'sent';
474
+ payout.txHash = result.txHash;
475
+ payout.sentAt = new Date().toISOString();
476
+ payout.error = undefined;
477
+ } catch (err: any) {
478
+ payout.error = err.message;
479
+ }
480
+ }
481
+
482
+ // Recalculate status
483
+ const statuses = split.payouts.map(p => p.status);
484
+ if (statuses.every(s => s === 'sent')) {
485
+ split.status = 'completed';
486
+ } else if (statuses.some(s => s === 'sent')) {
487
+ split.status = 'partial';
488
+ }
489
+
490
+ split.updatedAt = new Date().toISOString();
491
+ await this.saveSplits(splits);
492
+
493
+ return split;
494
+ }
495
+
496
+ // ============ Templates ============
497
+
498
+ /**
499
+ * Create a split template
500
+ */
501
+ async createTemplate(params: {
502
+ name: string;
503
+ description?: string;
504
+ recipients: SplitTemplate['recipients'];
505
+ }): Promise<SplitTemplate> {
506
+ const templates = await this.loadTemplates();
507
+
508
+ const template: SplitTemplate = {
509
+ id: crypto.randomUUID(),
510
+ name: params.name,
511
+ description: params.description,
512
+ recipients: params.recipients,
513
+ usageCount: 0,
514
+ createdAt: new Date().toISOString(),
515
+ };
516
+
517
+ templates.push(template);
518
+ await this.saveTemplates(templates);
519
+
520
+ return template;
521
+ }
522
+
523
+ /**
524
+ * List templates
525
+ */
526
+ async listTemplates(): Promise<SplitTemplate[]> {
527
+ return this.loadTemplates();
528
+ }
529
+
530
+ /**
531
+ * Get common RE templates
532
+ */
533
+ async getDefaultTemplates(): Promise<SplitTemplate[]> {
534
+ return [
535
+ {
536
+ id: 'default-70-30',
537
+ name: 'Standard 70/30 Split',
538
+ description: 'Agent gets 70%, broker gets 30%',
539
+ recipients: [
540
+ { role: 'listing_agent', splitType: 'percentage', splitValue: '70' },
541
+ { role: 'broker', splitType: 'percentage', splitValue: '30' },
542
+ ],
543
+ usageCount: 0,
544
+ createdAt: new Date().toISOString(),
545
+ },
546
+ {
547
+ id: 'default-80-20',
548
+ name: 'Senior Agent 80/20',
549
+ description: 'Senior agent split - 80% agent, 20% broker',
550
+ recipients: [
551
+ { role: 'listing_agent', splitType: 'percentage', splitValue: '80' },
552
+ { role: 'broker', splitType: 'percentage', splitValue: '20' },
553
+ ],
554
+ usageCount: 0,
555
+ createdAt: new Date().toISOString(),
556
+ },
557
+ {
558
+ id: 'default-referral',
559
+ name: 'Referral Split',
560
+ description: '25% to referral agent, remaining split 70/30',
561
+ recipients: [
562
+ { role: 'referral', splitType: 'percentage', splitValue: '25', tier: 1 },
563
+ { role: 'listing_agent', splitType: 'percentage', splitValue: '52.5', tier: 2 },
564
+ { role: 'broker', splitType: 'percentage', splitValue: '22.5', tier: 2 },
565
+ ],
566
+ usageCount: 0,
567
+ createdAt: new Date().toISOString(),
568
+ },
569
+ {
570
+ id: 'default-team',
571
+ name: 'Team Split',
572
+ description: 'Team lead, agent, broker, team fee',
573
+ recipients: [
574
+ { role: 'company', splitType: 'fixed', splitValue: '500', tier: 1 }, // Team fee
575
+ { role: 'listing_agent', splitType: 'percentage', splitValue: '50', tier: 2 },
576
+ { role: 'team_member', splitType: 'percentage', splitValue: '20', tier: 2 },
577
+ { role: 'broker', splitType: 'percentage', splitValue: '30', tier: 2 },
578
+ ],
579
+ usageCount: 0,
580
+ createdAt: new Date().toISOString(),
581
+ },
582
+ ];
583
+ }
584
+
585
+ // ============ Queries ============
586
+
587
+ /**
588
+ * Get split by ID
589
+ */
590
+ async get(id: string): Promise<CommissionSplit | null> {
591
+ const splits = await this.loadSplits();
592
+ return splits.find(s => s.id === id) || null;
593
+ }
594
+
595
+ /**
596
+ * List splits with filters
597
+ */
598
+ async list(filters?: {
599
+ status?: CommissionSplit['status'];
600
+ recipientAddress?: string;
601
+ fromDate?: string;
602
+ toDate?: string;
603
+ }): Promise<CommissionSplit[]> {
604
+ let splits = await this.loadSplits();
605
+
606
+ if (filters?.status) {
607
+ splits = splits.filter(s => s.status === filters.status);
608
+ }
609
+ if (filters?.recipientAddress) {
610
+ splits = splits.filter(s =>
611
+ s.recipients.some(r =>
612
+ r.walletAddress.toLowerCase() === filters.recipientAddress!.toLowerCase()
613
+ )
614
+ );
615
+ }
616
+ if (filters?.fromDate) {
617
+ splits = splits.filter(s => s.createdAt >= filters.fromDate!);
618
+ }
619
+ if (filters?.toDate) {
620
+ splits = splits.filter(s => s.createdAt <= filters.toDate!);
621
+ }
622
+
623
+ return splits.sort((a, b) =>
624
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
625
+ );
626
+ }
627
+
628
+ /**
629
+ * Get pending splits (ready but not executed)
630
+ */
631
+ async getPending(): Promise<CommissionSplit[]> {
632
+ return this.list({ status: 'ready' });
633
+ }
634
+
635
+ /**
636
+ * Get splits due for execution
637
+ */
638
+ async getDue(): Promise<CommissionSplit[]> {
639
+ const splits = await this.list({ status: 'ready' });
640
+ const now = new Date().toISOString();
641
+
642
+ return splits.filter(s => !s.executeAt || s.executeAt <= now);
643
+ }
644
+
645
+ // ============ Formatting ============
646
+
647
+ /**
648
+ * Format split summary
649
+ */
650
+ formatSplitSummary(split: CommissionSplit): string {
651
+ const statusEmoji = {
652
+ draft: '📝',
653
+ ready: '✅',
654
+ processing: '⏳',
655
+ completed: '✓',
656
+ partial: '⚠️',
657
+ failed: '❌',
658
+ }[split.status];
659
+
660
+ let summary = `${statusEmoji} **Commission Split ${split.id}**\n\n`;
661
+
662
+ if (split.propertyAddress) {
663
+ summary += `📍 ${split.propertyAddress}\n`;
664
+ }
665
+
666
+ summary += `💵 Total: **$${split.totalCommission} USDC**\n`;
667
+ summary += `Status: ${split.status.toUpperCase()}\n\n`;
668
+
669
+ summary += `**Payouts:**\n`;
670
+ for (const payout of split.payouts) {
671
+ const recipient = split.recipients.find(r => r.id === payout.recipientId);
672
+ const payoutEmoji = payout.status === 'sent' ? '✓' : payout.status === 'failed' ? '✗' : '○';
673
+ summary += `${payoutEmoji} ${recipient?.name} (${recipient?.role}): $${payout.amount}\n`;
674
+ }
675
+
676
+ return summary;
677
+ }
678
+ }
679
+
680
+ export default CommissionSplitter;