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.
- package/README.md +401 -0
- package/README.md.bak +401 -0
- package/dist/agent.d.ts +132 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +224 -0
- package/dist/agent.js.map +1 -0
- package/dist/analytics.d.ts +120 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +345 -0
- package/dist/analytics.js.map +1 -0
- package/dist/approvals.d.ts +168 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +406 -0
- package/dist/approvals.js.map +1 -0
- package/dist/circle-client.d.ts +152 -0
- package/dist/circle-client.d.ts.map +1 -0
- package/dist/circle-client.js +266 -0
- package/dist/circle-client.js.map +1 -0
- package/dist/commission.d.ts +191 -0
- package/dist/commission.d.ts.map +1 -0
- package/dist/commission.js +475 -0
- package/dist/commission.js.map +1 -0
- package/dist/condition-builder.d.ts +98 -0
- package/dist/condition-builder.d.ts.map +1 -0
- package/dist/condition-builder.js +193 -0
- package/dist/condition-builder.js.map +1 -0
- package/dist/contacts.d.ts +179 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +445 -0
- package/dist/contacts.js.map +1 -0
- package/dist/easy.d.ts +22 -0
- package/dist/easy.d.ts.map +1 -0
- package/dist/easy.js +40 -0
- package/dist/easy.js.map +1 -0
- package/dist/erc8004/constants.d.ts +152 -0
- package/dist/erc8004/constants.d.ts.map +1 -0
- package/dist/erc8004/constants.js +114 -0
- package/dist/erc8004/constants.js.map +1 -0
- package/dist/erc8004/discovery.d.ts +84 -0
- package/dist/erc8004/discovery.d.ts.map +1 -0
- package/dist/erc8004/discovery.js +217 -0
- package/dist/erc8004/discovery.js.map +1 -0
- package/dist/erc8004/identity.d.ts +91 -0
- package/dist/erc8004/identity.d.ts.map +1 -0
- package/dist/erc8004/identity.js +250 -0
- package/dist/erc8004/identity.js.map +1 -0
- package/dist/erc8004/index.d.ts +147 -0
- package/dist/erc8004/index.d.ts.map +1 -0
- package/dist/erc8004/index.js +225 -0
- package/dist/erc8004/index.js.map +1 -0
- package/dist/erc8004/reputation.d.ts +133 -0
- package/dist/erc8004/reputation.d.ts.map +1 -0
- package/dist/erc8004/reputation.js +277 -0
- package/dist/erc8004/reputation.js.map +1 -0
- package/dist/escrow-templates.d.ts +38 -0
- package/dist/escrow-templates.d.ts.map +1 -0
- package/dist/escrow-templates.js +419 -0
- package/dist/escrow-templates.js.map +1 -0
- package/dist/escrow.d.ts +320 -0
- package/dist/escrow.d.ts.map +1 -0
- package/dist/escrow.js +854 -0
- package/dist/escrow.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/invoices.d.ts +212 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +393 -0
- package/dist/invoices.js.map +1 -0
- package/dist/notifications.d.ts +141 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +350 -0
- package/dist/notifications.js.map +1 -0
- package/dist/tips.d.ts +171 -0
- package/dist/tips.d.ts.map +1 -0
- package/dist/tips.js +390 -0
- package/dist/tips.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/x402-client.d.ts +127 -0
- package/dist/x402-client.d.ts.map +1 -0
- package/dist/x402-client.js +350 -0
- package/dist/x402-client.js.map +1 -0
- package/dist/x402-server.d.ts +133 -0
- package/dist/x402-server.d.ts.map +1 -0
- package/dist/x402-server.js +330 -0
- package/dist/x402-server.js.map +1 -0
- package/lib/agent.ts +273 -0
- package/lib/analytics.ts +474 -0
- package/lib/analytics.ts.bak +474 -0
- package/lib/approvals.ts +585 -0
- package/lib/approvals.ts.bak +585 -0
- package/lib/circle-client.ts +376 -0
- package/lib/circle-client.ts.bak +376 -0
- package/lib/commission.ts +680 -0
- package/lib/commission.ts.bak +680 -0
- package/lib/condition-builder.ts +223 -0
- package/lib/condition-builder.ts.bak +223 -0
- package/lib/contacts.ts +615 -0
- package/lib/contacts.ts.bak +615 -0
- package/lib/easy.ts +46 -0
- package/lib/easy.ts.bak +352 -0
- package/lib/erc8004/constants.ts +175 -0
- package/lib/erc8004/discovery.ts +299 -0
- package/lib/erc8004/identity.ts +327 -0
- package/lib/erc8004/index.ts +285 -0
- package/lib/erc8004/reputation.ts +368 -0
- package/lib/escrow-templates.ts +462 -0
- package/lib/escrow.ts +1216 -0
- package/lib/index.ts +13 -0
- package/lib/invoices.ts +588 -0
- package/lib/notifications.ts +484 -0
- package/lib/tips.ts +570 -0
- package/lib/types.ts +108 -0
- package/lib/x402-client.ts +471 -0
- package/lib/x402-server.ts +462 -0
- package/package.json +58 -0
package/lib/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pay Lobster - Payment Infrastructure for AI Agents
|
|
3
|
+
* The Stripe for autonomous agents on Base
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { LobsterAgent } from './agent';
|
|
9
|
+
export { createLobsterAgent, quickStart } from './easy';
|
|
10
|
+
export * from './types';
|
|
11
|
+
|
|
12
|
+
// Default export for convenience
|
|
13
|
+
export { LobsterAgent as default } from './agent';
|
package/lib/invoices.ts
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoice & Payment Request System
|
|
3
|
+
*
|
|
4
|
+
* Generate, track, and manage USDC invoices and payment requests.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
export interface Invoice {
|
|
12
|
+
id: string;
|
|
13
|
+
number: string;
|
|
14
|
+
status: 'draft' | 'sent' | 'viewed' | 'paid' | 'cancelled' | 'overdue';
|
|
15
|
+
|
|
16
|
+
// Parties
|
|
17
|
+
from: {
|
|
18
|
+
name: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
walletAddress: string;
|
|
21
|
+
};
|
|
22
|
+
to: {
|
|
23
|
+
name: string;
|
|
24
|
+
email?: string;
|
|
25
|
+
walletAddress?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Amounts
|
|
29
|
+
items: InvoiceItem[];
|
|
30
|
+
subtotal: string;
|
|
31
|
+
tax?: string;
|
|
32
|
+
taxRate?: number;
|
|
33
|
+
total: string;
|
|
34
|
+
currency: 'USDC';
|
|
35
|
+
|
|
36
|
+
// Payment
|
|
37
|
+
paymentChain: string;
|
|
38
|
+
paymentAddress: string;
|
|
39
|
+
paymentLink?: string;
|
|
40
|
+
x402PaymentUrl?: string; // x402-enabled payment URL
|
|
41
|
+
txHash?: string;
|
|
42
|
+
paidAt?: string;
|
|
43
|
+
paidAmount?: string;
|
|
44
|
+
|
|
45
|
+
// Metadata
|
|
46
|
+
memo?: string;
|
|
47
|
+
dueDate?: string;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
sentAt?: string;
|
|
51
|
+
viewedAt?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface InvoiceItem {
|
|
55
|
+
description: string;
|
|
56
|
+
quantity: number;
|
|
57
|
+
unitPrice: string;
|
|
58
|
+
total: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PaymentRequest {
|
|
62
|
+
id: string;
|
|
63
|
+
type: 'one-time' | 'recurring';
|
|
64
|
+
amount: string;
|
|
65
|
+
description: string;
|
|
66
|
+
fromName: string;
|
|
67
|
+
toAddress: string;
|
|
68
|
+
chain: string;
|
|
69
|
+
status: 'pending' | 'paid' | 'cancelled' | 'expired';
|
|
70
|
+
expiresAt?: string;
|
|
71
|
+
paymentLink: string;
|
|
72
|
+
createdAt: string;
|
|
73
|
+
paidAt?: string;
|
|
74
|
+
txHash?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RecurringPayment {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
amount: string;
|
|
81
|
+
toAddress: string;
|
|
82
|
+
toName: string;
|
|
83
|
+
chain: string;
|
|
84
|
+
frequency: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'yearly';
|
|
85
|
+
nextPaymentDate: string;
|
|
86
|
+
startDate: string;
|
|
87
|
+
endDate?: string;
|
|
88
|
+
status: 'active' | 'paused' | 'cancelled' | 'completed';
|
|
89
|
+
walletId: string;
|
|
90
|
+
payments: { date: string; txHash: string; amount: string }[];
|
|
91
|
+
createdAt: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DATA_DIR = process.env.USDC_DATA_DIR || './data';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Invoice Manager
|
|
98
|
+
*/
|
|
99
|
+
export class InvoiceManager {
|
|
100
|
+
private dataPath: string;
|
|
101
|
+
|
|
102
|
+
constructor(dataDir = DATA_DIR) {
|
|
103
|
+
this.dataPath = path.join(dataDir, 'invoices.json');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async loadInvoices(): Promise<Invoice[]> {
|
|
107
|
+
try {
|
|
108
|
+
const data = await fs.readFile(this.dataPath, 'utf-8');
|
|
109
|
+
return JSON.parse(data);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async saveInvoices(invoices: Invoice[]): Promise<void> {
|
|
116
|
+
await fs.mkdir(path.dirname(this.dataPath), { recursive: true });
|
|
117
|
+
await fs.writeFile(this.dataPath, JSON.stringify(invoices, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a new invoice
|
|
122
|
+
*/
|
|
123
|
+
async create(params: {
|
|
124
|
+
from: Invoice['from'];
|
|
125
|
+
to: Invoice['to'];
|
|
126
|
+
items: { description: string; quantity: number; unitPrice: string }[];
|
|
127
|
+
taxRate?: number;
|
|
128
|
+
memo?: string;
|
|
129
|
+
dueDate?: string;
|
|
130
|
+
chain?: string;
|
|
131
|
+
}): Promise<Invoice> {
|
|
132
|
+
const invoices = await this.loadInvoices();
|
|
133
|
+
const invoiceNumber = `INV-${Date.now().toString(36).toUpperCase()}`;
|
|
134
|
+
|
|
135
|
+
const items: InvoiceItem[] = params.items.map(item => ({
|
|
136
|
+
...item,
|
|
137
|
+
total: (item.quantity * parseFloat(item.unitPrice)).toFixed(2),
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
const subtotal = items.reduce((sum, item) => sum + parseFloat(item.total), 0);
|
|
141
|
+
const tax = params.taxRate ? subtotal * (params.taxRate / 100) : 0;
|
|
142
|
+
const total = subtotal + tax;
|
|
143
|
+
|
|
144
|
+
const invoice: Invoice = {
|
|
145
|
+
id: crypto.randomUUID(),
|
|
146
|
+
number: invoiceNumber,
|
|
147
|
+
status: 'draft',
|
|
148
|
+
from: params.from,
|
|
149
|
+
to: params.to,
|
|
150
|
+
items,
|
|
151
|
+
subtotal: subtotal.toFixed(2),
|
|
152
|
+
tax: tax > 0 ? tax.toFixed(2) : undefined,
|
|
153
|
+
taxRate: params.taxRate,
|
|
154
|
+
total: total.toFixed(2),
|
|
155
|
+
currency: 'USDC',
|
|
156
|
+
paymentChain: params.chain || 'ETH-SEPOLIA',
|
|
157
|
+
paymentAddress: params.from.walletAddress,
|
|
158
|
+
memo: params.memo,
|
|
159
|
+
dueDate: params.dueDate,
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
updatedAt: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Generate payment link
|
|
165
|
+
invoice.paymentLink = this.generatePaymentLink(invoice);
|
|
166
|
+
|
|
167
|
+
// Generate x402 payment URL
|
|
168
|
+
invoice.x402PaymentUrl = this.generateX402PaymentUrl(invoice);
|
|
169
|
+
|
|
170
|
+
invoices.push(invoice);
|
|
171
|
+
await this.saveInvoices(invoices);
|
|
172
|
+
|
|
173
|
+
return invoice;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get invoice by ID or number
|
|
178
|
+
*/
|
|
179
|
+
async get(idOrNumber: string): Promise<Invoice | null> {
|
|
180
|
+
const invoices = await this.loadInvoices();
|
|
181
|
+
return invoices.find(inv =>
|
|
182
|
+
inv.id === idOrNumber || inv.number === idOrNumber
|
|
183
|
+
) || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List invoices with optional filters
|
|
188
|
+
*/
|
|
189
|
+
async list(filters?: {
|
|
190
|
+
status?: Invoice['status'];
|
|
191
|
+
toName?: string;
|
|
192
|
+
fromDate?: string;
|
|
193
|
+
toDate?: string;
|
|
194
|
+
}): Promise<Invoice[]> {
|
|
195
|
+
let invoices = await this.loadInvoices();
|
|
196
|
+
|
|
197
|
+
if (filters?.status) {
|
|
198
|
+
invoices = invoices.filter(inv => inv.status === filters.status);
|
|
199
|
+
}
|
|
200
|
+
if (filters?.toName) {
|
|
201
|
+
invoices = invoices.filter(inv =>
|
|
202
|
+
inv.to.name.toLowerCase().includes(filters.toName!.toLowerCase())
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (filters?.fromDate) {
|
|
206
|
+
invoices = invoices.filter(inv => inv.createdAt >= filters.fromDate!);
|
|
207
|
+
}
|
|
208
|
+
if (filters?.toDate) {
|
|
209
|
+
invoices = invoices.filter(inv => inv.createdAt <= filters.toDate!);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return invoices.sort((a, b) =>
|
|
213
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Mark invoice as sent
|
|
219
|
+
*/
|
|
220
|
+
async markSent(id: string): Promise<Invoice | null> {
|
|
221
|
+
const invoices = await this.loadInvoices();
|
|
222
|
+
const invoice = invoices.find(inv => inv.id === id);
|
|
223
|
+
|
|
224
|
+
if (invoice) {
|
|
225
|
+
invoice.status = 'sent';
|
|
226
|
+
invoice.sentAt = new Date().toISOString();
|
|
227
|
+
invoice.updatedAt = new Date().toISOString();
|
|
228
|
+
await this.saveInvoices(invoices);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return invoice || null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Mark invoice as paid
|
|
236
|
+
*/
|
|
237
|
+
async markPaid(id: string, txHash: string, amount?: string): Promise<Invoice | null> {
|
|
238
|
+
const invoices = await this.loadInvoices();
|
|
239
|
+
const invoice = invoices.find(inv => inv.id === id);
|
|
240
|
+
|
|
241
|
+
if (invoice) {
|
|
242
|
+
invoice.status = 'paid';
|
|
243
|
+
invoice.txHash = txHash;
|
|
244
|
+
invoice.paidAt = new Date().toISOString();
|
|
245
|
+
invoice.paidAmount = amount || invoice.total;
|
|
246
|
+
invoice.updatedAt = new Date().toISOString();
|
|
247
|
+
await this.saveInvoices(invoices);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return invoice || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Cancel invoice
|
|
255
|
+
*/
|
|
256
|
+
async cancel(id: string): Promise<Invoice | null> {
|
|
257
|
+
const invoices = await this.loadInvoices();
|
|
258
|
+
const invoice = invoices.find(inv => inv.id === id);
|
|
259
|
+
|
|
260
|
+
if (invoice && invoice.status !== 'paid') {
|
|
261
|
+
invoice.status = 'cancelled';
|
|
262
|
+
invoice.updatedAt = new Date().toISOString();
|
|
263
|
+
await this.saveInvoices(invoices);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return invoice || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Generate payment link for invoice
|
|
271
|
+
*/
|
|
272
|
+
private generatePaymentLink(invoice: Invoice): string {
|
|
273
|
+
// EIP-681 style payment request (simplified)
|
|
274
|
+
const params = new URLSearchParams({
|
|
275
|
+
to: invoice.paymentAddress,
|
|
276
|
+
value: invoice.total,
|
|
277
|
+
chain: invoice.paymentChain,
|
|
278
|
+
ref: invoice.number,
|
|
279
|
+
});
|
|
280
|
+
return `usdc://pay?${params.toString()}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate x402-enabled payment URL for invoice
|
|
285
|
+
* Returns a URL that triggers 402 Payment Required
|
|
286
|
+
*/
|
|
287
|
+
private generateX402PaymentUrl(invoice: Invoice): string {
|
|
288
|
+
// This would point to your x402-enabled invoice payment endpoint
|
|
289
|
+
const baseUrl = process.env.X402_BASE_URL || 'https://api.lobster-pay.com';
|
|
290
|
+
return `${baseUrl}/invoices/${invoice.id}/pay`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Add x402 payment method to invoice
|
|
295
|
+
* Creates a payment-gated endpoint for this invoice
|
|
296
|
+
*/
|
|
297
|
+
async enableX402Payment(
|
|
298
|
+
invoiceId: string,
|
|
299
|
+
options?: {
|
|
300
|
+
baseUrl?: string;
|
|
301
|
+
expiryHours?: number;
|
|
302
|
+
}
|
|
303
|
+
): Promise<string> {
|
|
304
|
+
const invoice = await this.get(invoiceId);
|
|
305
|
+
if (!invoice) {
|
|
306
|
+
throw new Error('Invoice not found');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const baseUrl = options?.baseUrl || process.env.X402_BASE_URL || 'https://api.lobster-pay.com';
|
|
310
|
+
const x402Url = `${baseUrl}/invoices/${invoice.id}/pay`;
|
|
311
|
+
|
|
312
|
+
invoice.x402PaymentUrl = x402Url;
|
|
313
|
+
invoice.updatedAt = new Date().toISOString();
|
|
314
|
+
|
|
315
|
+
const invoices = await this.loadInvoices();
|
|
316
|
+
const index = invoices.findIndex(inv => inv.id === invoiceId);
|
|
317
|
+
if (index !== -1) {
|
|
318
|
+
invoices[index] = invoice;
|
|
319
|
+
await this.saveInvoices(invoices);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return x402Url;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Format invoice as text for messaging
|
|
327
|
+
*/
|
|
328
|
+
formatInvoiceText(invoice: Invoice): string {
|
|
329
|
+
let text = `📄 **Invoice ${invoice.number}**\n\n`;
|
|
330
|
+
text += `To: ${invoice.to.name}\n`;
|
|
331
|
+
text += `Status: ${invoice.status.toUpperCase()}\n\n`;
|
|
332
|
+
|
|
333
|
+
text += `**Items:**\n`;
|
|
334
|
+
for (const item of invoice.items) {
|
|
335
|
+
text += `• ${item.description} (x${item.quantity}) — $${item.total}\n`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
text += `\nSubtotal: $${invoice.subtotal} USDC\n`;
|
|
339
|
+
if (invoice.tax) {
|
|
340
|
+
text += `Tax (${invoice.taxRate}%): $${invoice.tax} USDC\n`;
|
|
341
|
+
}
|
|
342
|
+
text += `**Total: $${invoice.total} USDC**\n\n`;
|
|
343
|
+
|
|
344
|
+
if (invoice.dueDate) {
|
|
345
|
+
text += `Due: ${new Date(invoice.dueDate).toLocaleDateString()}\n`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
text += `\nPay to: \`${invoice.paymentAddress}\`\n`;
|
|
349
|
+
text += `Chain: ${invoice.paymentChain}\n`;
|
|
350
|
+
|
|
351
|
+
if (invoice.memo) {
|
|
352
|
+
text += `\nMemo: ${invoice.memo}\n`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return text;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check for overdue invoices
|
|
360
|
+
*/
|
|
361
|
+
async checkOverdue(): Promise<Invoice[]> {
|
|
362
|
+
const invoices = await this.loadInvoices();
|
|
363
|
+
const now = new Date();
|
|
364
|
+
const overdue: Invoice[] = [];
|
|
365
|
+
|
|
366
|
+
for (const invoice of invoices) {
|
|
367
|
+
if (
|
|
368
|
+
invoice.status === 'sent' &&
|
|
369
|
+
invoice.dueDate &&
|
|
370
|
+
new Date(invoice.dueDate) < now
|
|
371
|
+
) {
|
|
372
|
+
invoice.status = 'overdue';
|
|
373
|
+
invoice.updatedAt = now.toISOString();
|
|
374
|
+
overdue.push(invoice);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (overdue.length > 0) {
|
|
379
|
+
await this.saveInvoices(invoices);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return overdue;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Recurring Payment Manager
|
|
388
|
+
*/
|
|
389
|
+
export class RecurringPaymentManager {
|
|
390
|
+
private dataPath: string;
|
|
391
|
+
|
|
392
|
+
constructor(dataDir = DATA_DIR) {
|
|
393
|
+
this.dataPath = path.join(dataDir, 'recurring.json');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private async loadPayments(): Promise<RecurringPayment[]> {
|
|
397
|
+
try {
|
|
398
|
+
const data = await fs.readFile(this.dataPath, 'utf-8');
|
|
399
|
+
return JSON.parse(data);
|
|
400
|
+
} catch {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private async savePayments(payments: RecurringPayment[]): Promise<void> {
|
|
406
|
+
await fs.mkdir(path.dirname(this.dataPath), { recursive: true });
|
|
407
|
+
await fs.writeFile(this.dataPath, JSON.stringify(payments, null, 2));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Schedule a recurring payment
|
|
412
|
+
*/
|
|
413
|
+
async schedule(params: {
|
|
414
|
+
name: string;
|
|
415
|
+
amount: string;
|
|
416
|
+
toAddress: string;
|
|
417
|
+
toName: string;
|
|
418
|
+
chain: string;
|
|
419
|
+
frequency: RecurringPayment['frequency'];
|
|
420
|
+
startDate?: string;
|
|
421
|
+
endDate?: string;
|
|
422
|
+
walletId: string;
|
|
423
|
+
}): Promise<RecurringPayment> {
|
|
424
|
+
const payments = await this.loadPayments();
|
|
425
|
+
|
|
426
|
+
const startDate = params.startDate || new Date().toISOString().split('T')[0];
|
|
427
|
+
const nextPaymentDate = this.calculateNextDate(startDate, params.frequency);
|
|
428
|
+
|
|
429
|
+
const payment: RecurringPayment = {
|
|
430
|
+
id: crypto.randomUUID(),
|
|
431
|
+
name: params.name,
|
|
432
|
+
amount: params.amount,
|
|
433
|
+
toAddress: params.toAddress,
|
|
434
|
+
toName: params.toName,
|
|
435
|
+
chain: params.chain,
|
|
436
|
+
frequency: params.frequency,
|
|
437
|
+
startDate,
|
|
438
|
+
endDate: params.endDate,
|
|
439
|
+
nextPaymentDate,
|
|
440
|
+
status: 'active',
|
|
441
|
+
walletId: params.walletId,
|
|
442
|
+
payments: [],
|
|
443
|
+
createdAt: new Date().toISOString(),
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
payments.push(payment);
|
|
447
|
+
await this.savePayments(payments);
|
|
448
|
+
|
|
449
|
+
return payment;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get all due payments
|
|
454
|
+
*/
|
|
455
|
+
async getDuePayments(): Promise<RecurringPayment[]> {
|
|
456
|
+
const payments = await this.loadPayments();
|
|
457
|
+
const today = new Date().toISOString().split('T')[0];
|
|
458
|
+
|
|
459
|
+
return payments.filter(p =>
|
|
460
|
+
p.status === 'active' &&
|
|
461
|
+
p.nextPaymentDate <= today &&
|
|
462
|
+
(!p.endDate || p.endDate >= today)
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Record a payment execution
|
|
468
|
+
*/
|
|
469
|
+
async recordExecution(id: string, txHash: string): Promise<RecurringPayment | null> {
|
|
470
|
+
const payments = await this.loadPayments();
|
|
471
|
+
const payment = payments.find(p => p.id === id);
|
|
472
|
+
|
|
473
|
+
if (payment) {
|
|
474
|
+
payment.payments.push({
|
|
475
|
+
date: new Date().toISOString(),
|
|
476
|
+
txHash,
|
|
477
|
+
amount: payment.amount,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Calculate next payment date
|
|
481
|
+
payment.nextPaymentDate = this.calculateNextDate(
|
|
482
|
+
payment.nextPaymentDate,
|
|
483
|
+
payment.frequency
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Check if completed
|
|
487
|
+
if (payment.endDate && payment.nextPaymentDate > payment.endDate) {
|
|
488
|
+
payment.status = 'completed';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await this.savePayments(payments);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return payment || null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Pause recurring payment
|
|
499
|
+
*/
|
|
500
|
+
async pause(id: string): Promise<RecurringPayment | null> {
|
|
501
|
+
const payments = await this.loadPayments();
|
|
502
|
+
const payment = payments.find(p => p.id === id);
|
|
503
|
+
|
|
504
|
+
if (payment && payment.status === 'active') {
|
|
505
|
+
payment.status = 'paused';
|
|
506
|
+
await this.savePayments(payments);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return payment || null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Resume recurring payment
|
|
514
|
+
*/
|
|
515
|
+
async resume(id: string): Promise<RecurringPayment | null> {
|
|
516
|
+
const payments = await this.loadPayments();
|
|
517
|
+
const payment = payments.find(p => p.id === id);
|
|
518
|
+
|
|
519
|
+
if (payment && payment.status === 'paused') {
|
|
520
|
+
payment.status = 'active';
|
|
521
|
+
// Recalculate next payment from today
|
|
522
|
+
payment.nextPaymentDate = this.calculateNextDate(
|
|
523
|
+
new Date().toISOString().split('T')[0],
|
|
524
|
+
payment.frequency
|
|
525
|
+
);
|
|
526
|
+
await this.savePayments(payments);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return payment || null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Cancel recurring payment
|
|
534
|
+
*/
|
|
535
|
+
async cancel(id: string): Promise<RecurringPayment | null> {
|
|
536
|
+
const payments = await this.loadPayments();
|
|
537
|
+
const payment = payments.find(p => p.id === id);
|
|
538
|
+
|
|
539
|
+
if (payment) {
|
|
540
|
+
payment.status = 'cancelled';
|
|
541
|
+
await this.savePayments(payments);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return payment || null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* List all recurring payments
|
|
549
|
+
*/
|
|
550
|
+
async list(status?: RecurringPayment['status']): Promise<RecurringPayment[]> {
|
|
551
|
+
const payments = await this.loadPayments();
|
|
552
|
+
return status
|
|
553
|
+
? payments.filter(p => p.status === status)
|
|
554
|
+
: payments;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Calculate next payment date based on frequency
|
|
559
|
+
*/
|
|
560
|
+
private calculateNextDate(fromDate: string, frequency: RecurringPayment['frequency']): string {
|
|
561
|
+
const date = new Date(fromDate);
|
|
562
|
+
|
|
563
|
+
switch (frequency) {
|
|
564
|
+
case 'daily':
|
|
565
|
+
date.setDate(date.getDate() + 1);
|
|
566
|
+
break;
|
|
567
|
+
case 'weekly':
|
|
568
|
+
date.setDate(date.getDate() + 7);
|
|
569
|
+
break;
|
|
570
|
+
case 'biweekly':
|
|
571
|
+
date.setDate(date.getDate() + 14);
|
|
572
|
+
break;
|
|
573
|
+
case 'monthly':
|
|
574
|
+
date.setMonth(date.getMonth() + 1);
|
|
575
|
+
break;
|
|
576
|
+
case 'quarterly':
|
|
577
|
+
date.setMonth(date.getMonth() + 3);
|
|
578
|
+
break;
|
|
579
|
+
case 'yearly':
|
|
580
|
+
date.setFullYear(date.getFullYear() + 1);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return date.toISOString().split('T')[0];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export default { InvoiceManager, RecurringPaymentManager };
|