@profullstack/coinpay 0.18.0 → 0.20.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/CHANGELOG.md +15 -0
- package/bin/coinpay.js +247 -1
- package/package.json +1 -1
- package/src/contractors.d.ts +124 -0
- package/src/contractors.js +135 -0
- package/src/finances-commands.js +15 -4
- package/src/finances-reports.js +15 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +39 -0
- package/src/mcp.js +68 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.19.0] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Contractors: `coinpay contractor list|add|get|link|offboard|contract|send|end|pending|approve|reject|bonus|expense|1099`.
|
|
12
|
+
Invite a contractor (they get a private portal to add a payout wallet, file a W-9 or W-8,
|
|
13
|
+
sign contracts and submit hours, milestones and expenses), contract fixed, hourly or
|
|
14
|
+
milestone work, approve submissions, and see who needs a 1099-NEC.
|
|
15
|
+
- Pay runs: `coinpay payrun list|create|get|pay|record|delete`. A run collects fixed fees
|
|
16
|
+
that are due plus approved work, one line per contractor. `pay <id> --yes` sends from the
|
|
17
|
+
business payout wallet (signed-in owner only); `record` logs a payment made from your own wallet.
|
|
18
|
+
- SDK: `listContractors`, `addContractor`, `createContract`, `sendContract`, `reviewWorkItem`,
|
|
19
|
+
`createPayRun`, `payPayRun`, `recordPayRunPayments`, `get1099Summary` and friends.
|
|
20
|
+
- MCP: `contractor_list`, `contractor_get`, `contractor_work_list`, `contractor_work_review`,
|
|
21
|
+
`pay_run_list`, `pay_run_get`, `pay_run_draft`, `contractor_1099_summary`. None of them sends money.
|
|
22
|
+
|
|
8
23
|
## [0.18.0] - 2026-10-17
|
|
9
24
|
|
|
10
25
|
### Added
|
package/bin/coinpay.js
CHANGED
|
@@ -46,6 +46,26 @@ import {
|
|
|
46
46
|
listTimeEntries,
|
|
47
47
|
invoiceProjectTime,
|
|
48
48
|
} from '../src/projects.js';
|
|
49
|
+
import {
|
|
50
|
+
listContractors,
|
|
51
|
+
getContractor,
|
|
52
|
+
addContractor,
|
|
53
|
+
createContractorPortalLink,
|
|
54
|
+
offboardContractor,
|
|
55
|
+
createContract,
|
|
56
|
+
sendContract,
|
|
57
|
+
endContract,
|
|
58
|
+
listWorkItems,
|
|
59
|
+
reviewWorkItem,
|
|
60
|
+
addContractorPayment,
|
|
61
|
+
listPayRuns,
|
|
62
|
+
getPayRun,
|
|
63
|
+
createPayRun,
|
|
64
|
+
deletePayRun,
|
|
65
|
+
payPayRun,
|
|
66
|
+
recordPayRunPayments,
|
|
67
|
+
get1099Summary,
|
|
68
|
+
} from '../src/contractors.js';
|
|
49
69
|
import { readFileSync, writeFileSync, existsSync, unlinkSync, chmodSync } from 'fs';
|
|
50
70
|
import { execSync, spawn } from 'child_process';
|
|
51
71
|
import { createInterface } from 'readline';
|
|
@@ -500,6 +520,28 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
500
520
|
portal-link <client-id> Create a portal link listing all their invoices
|
|
501
521
|
(replaces the old link; --revoke to turn it off)
|
|
502
522
|
|
|
523
|
+
${colors.bright}contractor${colors.reset}
|
|
524
|
+
list Contractors (--business-id, --all includes offboarded)
|
|
525
|
+
add --name, --email [--country US] [--title] [--no-invite]
|
|
526
|
+
get <id> Wallet, tax form, contracts and work lines
|
|
527
|
+
link <id> New portal link (--email to send it)
|
|
528
|
+
contract <id> Draft: --title --kind fixed|hourly --rate 2000 [--frequency monthly]
|
|
529
|
+
send <contract-id> Sign as the business and send: --signature "Your Name"
|
|
530
|
+
end <contract-id> [--reason] [--end YYYY-MM-DD]
|
|
531
|
+
pending Work waiting for approval
|
|
532
|
+
approve <id>... Approve work lines; reject <id> --note "why"
|
|
533
|
+
bonus | expense --contract <id> --amount 250 --description "..."
|
|
534
|
+
1099 Totals and who needs a 1099-NEC (--year 2026)
|
|
535
|
+
offboard <id> End contracts and revoke the portal link
|
|
536
|
+
|
|
537
|
+
${colors.bright}payrun${colors.reset}
|
|
538
|
+
list Pay runs
|
|
539
|
+
create Draft from fixed fees due + approved work [--date YYYY-MM-DD]
|
|
540
|
+
get <id> Per-contractor amounts, lines and warnings
|
|
541
|
+
pay <id> --yes Send from the business payout wallet (owner login only)
|
|
542
|
+
record <id> You paid yourself: --item <item-id> --tx <hash>
|
|
543
|
+
delete <id> Discard a draft
|
|
544
|
+
|
|
503
545
|
${colors.bright}project${colors.reset}
|
|
504
546
|
list Projects (--business-id, --archived)
|
|
505
547
|
create --name, [--client-id] [--rate 120] [--budget 40]
|
|
@@ -640,7 +682,7 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
640
682
|
due [--days 7] What is due and overdue across every account (--days up to 60)
|
|
641
683
|
recurring Every recurring charge detected, with next date and monthly cost
|
|
642
684
|
due-day <id> <1-31|clear> A card/loan's statement due day (--amount 810.46, or balance)
|
|
643
|
-
daily [show|on|off|send-now] Daily summary email:
|
|
685
|
+
daily [show|on|off|send-now] Daily summary email: spend vs target, top cuts, due soon, position (--hour 7 --to …, --target 100)
|
|
644
686
|
|
|
645
687
|
${colors.bright}escrow${colors.reset}
|
|
646
688
|
create Create a new escrow
|
|
@@ -1228,6 +1270,200 @@ function hoursOf(minutes) {
|
|
|
1228
1270
|
return minutes == null ? 'running' : Math.floor(minutes / 60) + 'h' + String(minutes % 60).padStart(2, '0') + 'm';
|
|
1229
1271
|
}
|
|
1230
1272
|
|
|
1273
|
+
const usdText = (n) => '$' + Number(n).toFixed(2);
|
|
1274
|
+
|
|
1275
|
+
function usageExit(usage) {
|
|
1276
|
+
print.error('Usage: ' + usage);
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function handleContractor(subcommand, args, flags) {
|
|
1281
|
+
const client = createClient();
|
|
1282
|
+
requireFlagValues(flags, ['business-id', 'name', 'email', 'country', 'title', 'kind', 'rate', 'frequency', 'start', 'end', 'note', 'signature', 'reason', 'amount', 'contract', 'description', 'status', 'year']);
|
|
1283
|
+
switch (subcommand) {
|
|
1284
|
+
case 'list': {
|
|
1285
|
+
const rows = await listContractors(client, { businessId: flags['business-id'], includeOffboarded: !!flags.all });
|
|
1286
|
+
if (flags.json) return print.json(rows);
|
|
1287
|
+
if (!rows.length) return print.info('No contractors');
|
|
1288
|
+
for (const c of rows) {
|
|
1289
|
+
const bits = [c.status, c.payout_currency || 'no wallet', c.tax_form ? c.tax_form + ' ' + c.tax_status : 'no tax form'];
|
|
1290
|
+
if (c.pending_approvals) bits.push(c.pending_approvals + ' to approve');
|
|
1291
|
+
print.info(c.id + ' ' + c.name + ' <' + c.email + '> ' + bits.join(' · '));
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
case 'add': {
|
|
1296
|
+
if (!flags.name || !flags.email) usageExit('coinpay contractor add --name "Ada" --email ada@example.com [--country US] [--no-invite]');
|
|
1297
|
+
const result = await addContractor(client, {
|
|
1298
|
+
businessId: flags['business-id'],
|
|
1299
|
+
name: flags.name,
|
|
1300
|
+
email: flags.email,
|
|
1301
|
+
country: flags.country?.toUpperCase(),
|
|
1302
|
+
jobTitle: flags.title,
|
|
1303
|
+
sendInvite: !flags['no-invite'],
|
|
1304
|
+
});
|
|
1305
|
+
if (flags.json) return print.json(result);
|
|
1306
|
+
print.success('Contractor added: ' + result.contractor.id + (flags['no-invite'] ? '' : ' (invite emailed)'));
|
|
1307
|
+
return print.info('Portal link: ' + result.portal_url);
|
|
1308
|
+
}
|
|
1309
|
+
case 'get': {
|
|
1310
|
+
if (args.length !== 1) usageExit('coinpay contractor get <id>');
|
|
1311
|
+
const data = await getContractor(client, args[0]);
|
|
1312
|
+
if (flags.json) return print.json(data);
|
|
1313
|
+
const c = data.contractor;
|
|
1314
|
+
print.info(c.name + ' <' + c.email + '> ' + c.status);
|
|
1315
|
+
print.info('Wallet: ' + (c.payout_wallet ? c.payout_currency + ' ' + c.payout_wallet : 'not set'));
|
|
1316
|
+
print.info('Tax form: ' + (c.tax_form ? c.tax_form + ' ' + c.tax_status + (c.tax_id_last4 ? ' •••' + c.tax_id_last4 : '') : 'missing'));
|
|
1317
|
+
for (const k of data.contracts) {
|
|
1318
|
+
const terms = k.kind === 'fixed' ? usdText(k.rate) + ' ' + k.frequency : k.kind === 'hourly' ? usdText(k.rate) + '/h' : 'milestones';
|
|
1319
|
+
print.info(' contract ' + k.id + ' ' + k.title + ' ' + terms + ' ' + k.status);
|
|
1320
|
+
}
|
|
1321
|
+
for (const w of data.work_items.slice(0, 20)) print.info(' ' + w.work_date + ' ' + w.kind + ' ' + usdText(w.amount) + ' ' + w.status + ' ' + w.id);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
case 'link': {
|
|
1325
|
+
if (args.length !== 1) usageExit('coinpay contractor link <id> [--email]');
|
|
1326
|
+
const url = await createContractorPortalLink(client, args[0], { sendEmail: !!flags.email });
|
|
1327
|
+
return flags.json ? print.json({ url }) : print.info(url);
|
|
1328
|
+
}
|
|
1329
|
+
case 'offboard': {
|
|
1330
|
+
if (args.length !== 1) usageExit('coinpay contractor offboard <id>');
|
|
1331
|
+
await offboardContractor(client, args[0]);
|
|
1332
|
+
return print.success('Offboarded');
|
|
1333
|
+
}
|
|
1334
|
+
case 'contract': {
|
|
1335
|
+
if (args.length !== 1 || !flags.title || !flags.kind) {
|
|
1336
|
+
usageExit('coinpay contractor contract <id> --title "Retainer" --kind fixed|hourly --rate 2000 [--frequency monthly] [--start YYYY-MM-DD]');
|
|
1337
|
+
}
|
|
1338
|
+
if (flags.kind === 'milestone') usageExit('milestone contracts are created in the dashboard (they need a milestone list)');
|
|
1339
|
+
const rate = parseNonNegativeAmount(String(flags.rate ?? ''));
|
|
1340
|
+
if (!rate) usageExit('--rate must be an amount with at most two decimals');
|
|
1341
|
+
const contract = await createContract(client, args[0], {
|
|
1342
|
+
title: flags.title,
|
|
1343
|
+
kind: flags.kind,
|
|
1344
|
+
rate,
|
|
1345
|
+
frequency: flags.frequency,
|
|
1346
|
+
startDate: flags.start,
|
|
1347
|
+
endDate: flags.end,
|
|
1348
|
+
});
|
|
1349
|
+
if (flags.json) return print.json(contract);
|
|
1350
|
+
print.success('Draft contract: ' + contract.id);
|
|
1351
|
+
return print.info('Send it with: coinpay contractor send ' + contract.id + ' --signature "Your Name"');
|
|
1352
|
+
}
|
|
1353
|
+
case 'send': {
|
|
1354
|
+
if (args.length !== 1 || !flags.signature) usageExit('coinpay contractor send <contract-id> --signature "Your Name"');
|
|
1355
|
+
const contract = await sendContract(client, args[0], { signature: flags.signature });
|
|
1356
|
+
return flags.json ? print.json(contract) : print.success('Signed and sent: ' + contract.title);
|
|
1357
|
+
}
|
|
1358
|
+
case 'end': {
|
|
1359
|
+
if (args.length !== 1) usageExit('coinpay contractor end <contract-id> [--reason "..."] [--end YYYY-MM-DD]');
|
|
1360
|
+
const contract = await endContract(client, args[0], { reason: flags.reason, endDate: flags.end });
|
|
1361
|
+
return flags.json ? print.json(contract) : print.success('Contract ended ' + contract.end_date);
|
|
1362
|
+
}
|
|
1363
|
+
case 'pending': {
|
|
1364
|
+
const rows = await listWorkItems(client, { businessId: flags['business-id'], status: flags.status || 'submitted' });
|
|
1365
|
+
if (flags.json) return print.json(rows);
|
|
1366
|
+
if (!rows.length) return print.info('Nothing waiting');
|
|
1367
|
+
for (const w of rows) {
|
|
1368
|
+
print.info(w.id + ' ' + (w.contractors?.name || '') + ' ' + w.kind + (w.minutes ? ' ' + (w.minutes / 60).toFixed(2) + 'h' : '') + ' ' + usdText(w.amount) + ' ' + w.description);
|
|
1369
|
+
}
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
case 'approve':
|
|
1373
|
+
case 'reject': {
|
|
1374
|
+
if (!args.length) usageExit('coinpay contractor ' + subcommand + ' <work-item-id>...' + (subcommand === 'reject' ? ' --note "why"' : ''));
|
|
1375
|
+
for (const workId of args) {
|
|
1376
|
+
const w = await reviewWorkItem(client, workId, { action: subcommand, note: flags.note });
|
|
1377
|
+
print.success((subcommand === 'approve' ? 'Approved ' : 'Rejected ') + w.id + ' ' + usdText(w.amount));
|
|
1378
|
+
}
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
case 'bonus':
|
|
1382
|
+
case 'expense': {
|
|
1383
|
+
if (!flags.contract || !flags.amount || !flags.description) {
|
|
1384
|
+
usageExit('coinpay contractor ' + subcommand + ' --contract <id> --amount 250 --description "..."');
|
|
1385
|
+
}
|
|
1386
|
+
const amount = parseNonNegativeAmount(String(flags.amount));
|
|
1387
|
+
if (!amount) usageExit('--amount must be an amount with at most two decimals');
|
|
1388
|
+
const w = await addContractorPayment(client, { businessId: flags['business-id'], contractId: flags.contract, kind: subcommand, amount, description: flags.description });
|
|
1389
|
+
return flags.json ? print.json(w) : print.success(subcommand + ' added: ' + usdText(w.amount) + ' (goes into the next pay run)');
|
|
1390
|
+
}
|
|
1391
|
+
case '1099': {
|
|
1392
|
+
const year = flags.year ? Number(flags.year) : new Date().getUTCFullYear() - 1;
|
|
1393
|
+
const data = await get1099Summary(client, { businessId: flags['business-id'], year });
|
|
1394
|
+
if (flags.json) return print.json(data);
|
|
1395
|
+
if (!data.contractors.length) return print.info('No contractor payments in ' + year);
|
|
1396
|
+
for (const r of data.contractors) {
|
|
1397
|
+
print.info(String(r.name).padEnd(30) + usdText(r.total_paid).padStart(14) + ' ' + (r.needs_1099_nec ? '1099-NEC' : r.missing_w9 ? 'MISSING W-9' : '-'));
|
|
1398
|
+
}
|
|
1399
|
+
return print.info('Full CSV with tax ids: the Contractors page, Tax forms & 1099 tab (owner only).');
|
|
1400
|
+
}
|
|
1401
|
+
default:
|
|
1402
|
+
usageExit('coinpay contractor <list|add|get|link|offboard|contract|send|end|pending|approve|reject|bonus|expense|1099>');
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function handlePayRun(subcommand, args, flags) {
|
|
1407
|
+
const client = createClient();
|
|
1408
|
+
requireFlagValues(flags, ['business-id', 'date', 'item', 'tx']);
|
|
1409
|
+
switch (subcommand) {
|
|
1410
|
+
case 'list': {
|
|
1411
|
+
const rows = await listPayRuns(client, { businessId: flags['business-id'] });
|
|
1412
|
+
if (flags.json) return print.json(rows);
|
|
1413
|
+
if (!rows.length) return print.info('No pay runs');
|
|
1414
|
+
for (const r of rows) print.info(r.id + ' #' + r.number + ' ' + r.pay_date + ' ' + usdText(r.total) + ' ' + r.status + ' ' + r.paid + '/' + r.contractors + ' paid');
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
case 'create': {
|
|
1418
|
+
const run = await createPayRun(client, { businessId: flags['business-id'], payDate: flags.date });
|
|
1419
|
+
if (flags.json) return print.json(run);
|
|
1420
|
+
print.success('Drafted pay run #' + run.number + ' for ' + usdText(run.total) + ': ' + run.id);
|
|
1421
|
+
return print.info('Review it with: coinpay payrun get ' + run.id);
|
|
1422
|
+
}
|
|
1423
|
+
case 'get': {
|
|
1424
|
+
if (args.length !== 1) usageExit('coinpay payrun get <id>');
|
|
1425
|
+
const data = await getPayRun(client, args[0]);
|
|
1426
|
+
if (flags.json) return print.json(data);
|
|
1427
|
+
const r = data.pay_run;
|
|
1428
|
+
print.info('Pay run #' + r.number + ' ' + r.pay_date + ' ' + usdText(r.total) + ' ' + r.status);
|
|
1429
|
+
for (const i of data.items) {
|
|
1430
|
+
print.info(' ' + i.id + ' ' + String(i.contractors?.name || '').padEnd(24) + usdText(i.amount).padStart(12) + ' ' + i.status + (i.tx_hash ? ' ' + i.tx_hash : '') + (i.can_send_from_wallet ? '' : ' (pay manually)'));
|
|
1431
|
+
for (const w of i.warnings || []) print.info(' ! ' + w);
|
|
1432
|
+
}
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
case 'pay': {
|
|
1436
|
+
if (args.length !== 1) usageExit('coinpay payrun pay <id> --yes (sends from the business payout wallet; needs `coinpay login` as the owner)');
|
|
1437
|
+
if (!flags.yes) {
|
|
1438
|
+
print.error('This sends crypto and cannot be undone. Review with `coinpay payrun get ' + args[0] + '`, then add --yes.');
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
const result = await payPayRun(client, args[0]);
|
|
1442
|
+
if (flags.json) return print.json(result);
|
|
1443
|
+
for (const o of result.outcomes) {
|
|
1444
|
+
const line = o.contractor + ': ' + o.status + (o.tx_hash ? ' ' + o.tx_hash : '') + (o.error ? ' · ' + o.error : '');
|
|
1445
|
+
if (o.error) print.error(line);
|
|
1446
|
+
else print.success(line);
|
|
1447
|
+
}
|
|
1448
|
+
return print.info('Pay run is ' + result.pay_run.status);
|
|
1449
|
+
}
|
|
1450
|
+
case 'record': {
|
|
1451
|
+
if (args.length !== 1 || !flags.item || !flags.tx) usageExit('coinpay payrun record <id> --item <item-id> --tx <hash> (you paid from your own wallet)');
|
|
1452
|
+
const result = await recordPayRunPayments(client, args[0], [{ itemId: flags.item, txHash: flags.tx }]);
|
|
1453
|
+
if (flags.json) return print.json(result);
|
|
1454
|
+
const o = result.outcomes[0];
|
|
1455
|
+
return o.error ? print.error(o.error) : print.success('Recorded. Pay run is ' + result.pay_run.status);
|
|
1456
|
+
}
|
|
1457
|
+
case 'delete': {
|
|
1458
|
+
if (args.length !== 1) usageExit('coinpay payrun delete <id>');
|
|
1459
|
+
await deletePayRun(client, args[0]);
|
|
1460
|
+
return print.success('Draft deleted; its lines go back to the approved pool');
|
|
1461
|
+
}
|
|
1462
|
+
default:
|
|
1463
|
+
usageExit('coinpay payrun <list|create|get|pay|record|delete>');
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1231
1467
|
async function handleProject(subcommand, args, flags) {
|
|
1232
1468
|
const client = createClient();
|
|
1233
1469
|
requireFlagValues(flags, ['business-id', 'name', 'client-id', 'rate', 'budget', 'currency']);
|
|
@@ -5021,6 +5257,16 @@ async function handleLightning(subcommand, args, flags) {
|
|
|
5021
5257
|
await handleProject(subcommand, args, flags);
|
|
5022
5258
|
break;
|
|
5023
5259
|
|
|
5260
|
+
case 'contractor':
|
|
5261
|
+
case 'contractors':
|
|
5262
|
+
await handleContractor(subcommand, args, flags);
|
|
5263
|
+
break;
|
|
5264
|
+
|
|
5265
|
+
case 'payrun':
|
|
5266
|
+
case 'payruns':
|
|
5267
|
+
await handlePayRun(subcommand, args, flags);
|
|
5268
|
+
break;
|
|
5269
|
+
|
|
5024
5270
|
case 'time':
|
|
5025
5271
|
await handleTime(subcommand, args, flags);
|
|
5026
5272
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/coinpay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { CoinPayClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
export interface Contractor {
|
|
4
|
+
id: string;
|
|
5
|
+
business_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
country: string | null;
|
|
9
|
+
job_title: string | null;
|
|
10
|
+
status: 'invited' | 'active' | 'offboarded';
|
|
11
|
+
payout_currency: string | null;
|
|
12
|
+
payout_wallet: string | null;
|
|
13
|
+
payout_wallet_changed_at: string | null;
|
|
14
|
+
tax_form: 'W9' | 'W8BEN' | 'W8BENE' | null;
|
|
15
|
+
tax_status: 'missing' | 'submitted' | 'verified';
|
|
16
|
+
tax_legal_name: string | null;
|
|
17
|
+
tax_id_last4: string | null;
|
|
18
|
+
pending_approvals?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ContractorMilestone {
|
|
22
|
+
id: string;
|
|
23
|
+
contract_id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
amount: string;
|
|
26
|
+
due_date: string | null;
|
|
27
|
+
position: number;
|
|
28
|
+
status: 'pending' | 'submitted' | 'approved' | 'paid';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ContractorContract {
|
|
32
|
+
id: string;
|
|
33
|
+
contractor_id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
scope: string | null;
|
|
36
|
+
kind: 'fixed' | 'hourly' | 'milestone';
|
|
37
|
+
rate: string | null;
|
|
38
|
+
frequency: 'weekly' | 'biweekly' | 'semimonthly' | 'monthly' | null;
|
|
39
|
+
start_date: string;
|
|
40
|
+
end_date: string | null;
|
|
41
|
+
next_pay_date: string | null;
|
|
42
|
+
status: 'draft' | 'sent' | 'active' | 'ended';
|
|
43
|
+
contractor_milestones?: ContractorMilestone[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ContractorWorkItem {
|
|
47
|
+
id: string;
|
|
48
|
+
contractor_id: string;
|
|
49
|
+
contract_id: string;
|
|
50
|
+
kind: 'hours' | 'milestone' | 'expense' | 'bonus' | 'fixed';
|
|
51
|
+
description: string;
|
|
52
|
+
work_date: string;
|
|
53
|
+
minutes: number | null;
|
|
54
|
+
amount: string;
|
|
55
|
+
status: 'submitted' | 'approved' | 'rejected' | 'paid';
|
|
56
|
+
review_note: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PayRun {
|
|
60
|
+
id: string;
|
|
61
|
+
business_id: string;
|
|
62
|
+
number: number;
|
|
63
|
+
pay_date: string;
|
|
64
|
+
status: 'draft' | 'processing' | 'paid' | 'partially_paid';
|
|
65
|
+
total: string;
|
|
66
|
+
auto_drafted: boolean;
|
|
67
|
+
paid_at: string | null;
|
|
68
|
+
contractors?: number;
|
|
69
|
+
paid?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PayRunItem {
|
|
73
|
+
id: string;
|
|
74
|
+
contractor_id: string;
|
|
75
|
+
amount: string;
|
|
76
|
+
status: 'pending' | 'processing' | 'paid' | 'failed' | 'indeterminate';
|
|
77
|
+
method: 'wallet' | 'manual' | null;
|
|
78
|
+
tx_hash: string | null;
|
|
79
|
+
error: string | null;
|
|
80
|
+
invoice_number: string;
|
|
81
|
+
warnings?: string[];
|
|
82
|
+
can_send_from_wallet?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PayOutcome {
|
|
86
|
+
item_id: string;
|
|
87
|
+
contractor: string;
|
|
88
|
+
status: string;
|
|
89
|
+
tx_hash?: string | null;
|
|
90
|
+
error?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function listContractors(client: CoinPayClient, options?: { businessId?: string; includeOffboarded?: boolean }): Promise<Contractor[]>;
|
|
94
|
+
export function getContractor(client: CoinPayClient, contractorId: string): Promise<{ contractor: Contractor; contracts: ContractorContract[]; work_items: ContractorWorkItem[]; payments: PayRunItem[] }>;
|
|
95
|
+
export function addContractor(client: CoinPayClient, input: { businessId?: string; name: string; email: string; country?: string; jobTitle?: string; sendInvite?: boolean }): Promise<{ contractor: Contractor; portal_url: string }>;
|
|
96
|
+
export function createContractorPortalLink(client: CoinPayClient, contractorId: string, options?: { sendEmail?: boolean }): Promise<string>;
|
|
97
|
+
export function offboardContractor(client: CoinPayClient, contractorId: string): Promise<{ success: boolean }>;
|
|
98
|
+
export function createContract(
|
|
99
|
+
client: CoinPayClient,
|
|
100
|
+
contractorId: string,
|
|
101
|
+
input: {
|
|
102
|
+
title: string;
|
|
103
|
+
kind: 'fixed' | 'hourly' | 'milestone';
|
|
104
|
+
scope?: string;
|
|
105
|
+
rate?: number;
|
|
106
|
+
frequency?: 'weekly' | 'biweekly' | 'semimonthly' | 'monthly';
|
|
107
|
+
startDate?: string;
|
|
108
|
+
endDate?: string;
|
|
109
|
+
firstPayDate?: string;
|
|
110
|
+
milestones?: { title: string; amount: number; due_date?: string }[];
|
|
111
|
+
}
|
|
112
|
+
): Promise<ContractorContract>;
|
|
113
|
+
export function sendContract(client: CoinPayClient, contractId: string, input: { signature: string }): Promise<ContractorContract>;
|
|
114
|
+
export function endContract(client: CoinPayClient, contractId: string, input?: { reason?: string; endDate?: string }): Promise<ContractorContract>;
|
|
115
|
+
export function listWorkItems(client: CoinPayClient, options?: { businessId?: string; status?: string; contractorId?: string }): Promise<ContractorWorkItem[]>;
|
|
116
|
+
export function reviewWorkItem(client: CoinPayClient, workItemId: string, input: { action: 'approve' | 'reject'; note?: string }): Promise<ContractorWorkItem>;
|
|
117
|
+
export function addContractorPayment(client: CoinPayClient, input: { businessId?: string; contractId: string; kind: 'bonus' | 'expense'; amount: number; description: string }): Promise<ContractorWorkItem>;
|
|
118
|
+
export function listPayRuns(client: CoinPayClient, options?: { businessId?: string }): Promise<PayRun[]>;
|
|
119
|
+
export function getPayRun(client: CoinPayClient, payRunId: string): Promise<{ pay_run: PayRun; items: PayRunItem[]; sendable_coins: string[] }>;
|
|
120
|
+
export function createPayRun(client: CoinPayClient, options?: { businessId?: string; payDate?: string }): Promise<PayRun>;
|
|
121
|
+
export function deletePayRun(client: CoinPayClient, payRunId: string): Promise<{ success: boolean }>;
|
|
122
|
+
export function payPayRun(client: CoinPayClient, payRunId: string, options?: { itemIds?: string[] }): Promise<{ success: boolean; pay_run: PayRun; outcomes: PayOutcome[] }>;
|
|
123
|
+
export function recordPayRunPayments(client: CoinPayClient, payRunId: string, payments: { itemId: string; txHash: string }[]): Promise<{ success: boolean; pay_run: PayRun; outcomes: PayOutcome[] }>;
|
|
124
|
+
export function get1099Summary(client: CoinPayClient, options?: { businessId?: string; year?: number }): Promise<{ year: number; threshold: number; contractors: { contractor_id: string; name: string; tax_form: string | null; total_paid: number; needs_1099_nec: boolean; missing_w9: boolean }[] }>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contractors and pay runs: invite contractors, contract with them, approve the
|
|
3
|
+
* work they submit and pay the whole team in crypto.
|
|
4
|
+
*
|
|
5
|
+
* An API key is locked to its own business, so `businessId` is optional when
|
|
6
|
+
* the client was created with one. Paying a run needs a signed-in owner
|
|
7
|
+
* (`coinpay login`), never an API key.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const q = (params) => {
|
|
11
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
12
|
+
return entries.length ? '?' + new URLSearchParams(entries.map(([k, v]) => [k, String(v)])).toString() : '';
|
|
13
|
+
};
|
|
14
|
+
const json = (method, body) => ({ method, body: JSON.stringify(body) });
|
|
15
|
+
const id = (v) => encodeURIComponent(v);
|
|
16
|
+
|
|
17
|
+
export async function listContractors(client, { businessId, includeOffboarded } = {}) {
|
|
18
|
+
const data = await client.request('/contractors' + q({ business_id: businessId, include_offboarded: includeOffboarded ? 'true' : undefined }));
|
|
19
|
+
return data.contractors ?? [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A contractor with contracts (and milestones), work lines and payments. */
|
|
23
|
+
export async function getContractor(client, contractorId) {
|
|
24
|
+
return client.request('/contractors/' + id(contractorId));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Invite a contractor. They are emailed a portal link unless sendInvite is false. */
|
|
28
|
+
export async function addContractor(client, { businessId, name, email, country, jobTitle, sendInvite }) {
|
|
29
|
+
const body = { name, email };
|
|
30
|
+
if (businessId) body.business_id = businessId;
|
|
31
|
+
if (country) body.country = country;
|
|
32
|
+
if (jobTitle) body.job_title = jobTitle;
|
|
33
|
+
if (sendInvite === false) body.send_invite = false;
|
|
34
|
+
return client.request('/contractors', json('POST', body));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Issue a new portal link (the old one stops working). Returns the URL. */
|
|
38
|
+
export async function createContractorPortalLink(client, contractorId, { sendEmail } = {}) {
|
|
39
|
+
const data = await client.request(`/contractors/${id(contractorId)}/portal-link`, json('POST', { send_email: !!sendEmail }));
|
|
40
|
+
return data.url;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Offboard: ends live contracts and revokes the portal link. Records are kept. */
|
|
44
|
+
export async function offboardContractor(client, contractorId) {
|
|
45
|
+
return client.request('/contractors/' + id(contractorId), { method: 'DELETE' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Draft a contract. kind: 'fixed' (rate per frequency), 'hourly' (rate per
|
|
50
|
+
* hour) or 'milestone' (milestones: [{ title, amount, due_date? }]).
|
|
51
|
+
*/
|
|
52
|
+
export async function createContract(client, contractorId, { title, scope, kind, rate, frequency, startDate, endDate, firstPayDate, milestones }) {
|
|
53
|
+
const body = { title, kind };
|
|
54
|
+
if (scope) body.scope = scope;
|
|
55
|
+
if (rate !== undefined) body.rate = rate;
|
|
56
|
+
if (frequency) body.frequency = frequency;
|
|
57
|
+
if (startDate) body.start_date = startDate;
|
|
58
|
+
if (endDate) body.end_date = endDate;
|
|
59
|
+
if (firstPayDate) body.first_pay_date = firstPayDate;
|
|
60
|
+
if (milestones) body.milestones = milestones;
|
|
61
|
+
const data = await client.request(`/contractors/${id(contractorId)}/contracts`, json('POST', body));
|
|
62
|
+
return data.contract;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Sign a draft contract as the business and send it to the contractor. */
|
|
66
|
+
export async function sendContract(client, contractId, { signature }) {
|
|
67
|
+
const data = await client.request('/contractor-contracts/' + id(contractId), json('PATCH', { action: 'send', signature }));
|
|
68
|
+
return data.contract;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function endContract(client, contractId, { reason, endDate } = {}) {
|
|
72
|
+
const data = await client.request('/contractor-contracts/' + id(contractId), json('PATCH', { action: 'end', reason, end_date: endDate }));
|
|
73
|
+
return data.contract;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Work lines: hours, milestones, expenses, bonuses and fixed fees. status: submitted|approved|rejected|paid */
|
|
77
|
+
export async function listWorkItems(client, { businessId, status, contractorId } = {}) {
|
|
78
|
+
const data = await client.request('/contractor-work-items' + q({ business_id: businessId, status, contractor_id: contractorId }));
|
|
79
|
+
return data.work_items ?? [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Approve or reject a submission. A rejection needs a note. */
|
|
83
|
+
export async function reviewWorkItem(client, workItemId, { action, note }) {
|
|
84
|
+
const data = await client.request('/contractor-work-items/' + id(workItemId), json('PATCH', { action, note }));
|
|
85
|
+
return data.work_item;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Add a bonus or an expense reimbursement (approved immediately). */
|
|
89
|
+
export async function addContractorPayment(client, { businessId, contractId, kind, amount, description }) {
|
|
90
|
+
const body = { contract_id: contractId, kind, amount, description };
|
|
91
|
+
if (businessId) body.business_id = businessId;
|
|
92
|
+
const data = await client.request('/contractor-work-items', json('POST', body));
|
|
93
|
+
return data.work_item;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function listPayRuns(client, { businessId } = {}) {
|
|
97
|
+
const data = await client.request('/pay-runs' + q({ business_id: businessId }));
|
|
98
|
+
return data.pay_runs ?? [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A run with one item per contractor, its lines, and any warnings. */
|
|
102
|
+
export async function getPayRun(client, payRunId) {
|
|
103
|
+
return client.request('/pay-runs/' + id(payRunId));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Draft a pay run (fixed fees due by payDate plus approved work). Moves no money. */
|
|
107
|
+
export async function createPayRun(client, { businessId, payDate } = {}) {
|
|
108
|
+
const body = {};
|
|
109
|
+
if (businessId) body.business_id = businessId;
|
|
110
|
+
if (payDate) body.pay_date = payDate;
|
|
111
|
+
const data = await client.request('/pay-runs', json('POST', body));
|
|
112
|
+
return data.pay_run;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function deletePayRun(client, payRunId) {
|
|
116
|
+
return client.request('/pay-runs/' + id(payRunId), { method: 'DELETE' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Send a run (or some items) from the business payout wallet. Owner session only. */
|
|
120
|
+
export async function payPayRun(client, payRunId, { itemIds } = {}) {
|
|
121
|
+
return client.request(`/pay-runs/${id(payRunId)}/pay`, json('POST', itemIds ? { method: 'wallet', item_ids: itemIds } : { method: 'wallet' }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Record payments made from your own wallet: [{ itemId, txHash }]. Owner session only. */
|
|
125
|
+
export async function recordPayRunPayments(client, payRunId, payments) {
|
|
126
|
+
return client.request(
|
|
127
|
+
`/pay-runs/${id(payRunId)}/pay`,
|
|
128
|
+
json('POST', { method: 'manual', payments: payments.map((p) => ({ item_id: p.itemId, tx_hash: p.txHash })) })
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Per-contractor totals for a calendar year and who needs a 1099-NEC. */
|
|
133
|
+
export async function get1099Summary(client, { businessId, year } = {}) {
|
|
134
|
+
return client.request('/contractors/1099' + q({ business_id: businessId, year }));
|
|
135
|
+
}
|
package/src/finances-commands.js
CHANGED
|
@@ -891,10 +891,21 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
891
891
|
const action = flags.off ? 'off' : args[0] || 'show';
|
|
892
892
|
const describe = (s) => s
|
|
893
893
|
? `Daily summary ${s.active ? 'on' : 'off'} · ${String(s.hour).padStart(2, '0')}:00 ${s.timezone} · to ${s.recipients.join(', ')}${s.nextRunAt ? ` · next ${s.nextRunAt}` : ''}${s.lastSentAt ? ` · last ${s.lastSentAt}` : ''}`
|
|
894
|
-
: 'No daily summary configured. Turn it on with: coinpay finances daily on --hour 7
|
|
894
|
+
: 'No daily summary configured. Turn it on with: coinpay finances daily on --hour 7';
|
|
895
|
+
const targetText = (t) => (t === null || t === undefined ? 'no daily spend target' : `target $${t}/day`);
|
|
896
|
+
if (flags.target !== undefined) {
|
|
897
|
+
const raw = String(flags.target);
|
|
898
|
+
const target = ['off', 'clear', 'none'].includes(raw) ? null : Number(raw.replace(/^\$/, ''));
|
|
899
|
+
if (target !== null && (!Number.isFinite(target) || target < 0)) throw new CliExit(EXIT.INVALID, 'The target is a daily amount, e.g. --target 100, or --target clear');
|
|
900
|
+
const data = await api.setDailySpendTarget(client, target);
|
|
901
|
+
if (action === 'show') {
|
|
902
|
+
emit({ schedule: data.schedule, target: data.target }, `${describe(data.schedule)}\n${target === null ? 'Cleared the daily spend target.' : `Daily spend target set to $${data.target}/day.`}`);
|
|
903
|
+
return EXIT.OK;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
895
906
|
if (action === 'show') {
|
|
896
|
-
const s = await api.
|
|
897
|
-
emit({ schedule: s }, describe(s));
|
|
907
|
+
const { schedule: s, target } = await api.getDailySummaryWithTarget(client);
|
|
908
|
+
emit({ schedule: s, target }, `${describe(s)} · ${targetText(target)}`);
|
|
898
909
|
return EXIT.OK;
|
|
899
910
|
}
|
|
900
911
|
if (action === 'on' || action === 'set') {
|
|
@@ -917,7 +928,7 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
|
|
|
917
928
|
emit(data, `Sent to ${data.sent.join(', ') || 'nobody'} (${data.due} due, ${data.overdue} overdue)${data.failed.length ? `; failed: ${data.failed.map((f) => `${f.to} (${f.error})`).join(', ')}` : ''}`);
|
|
918
929
|
return data.sent.length > 0 ? EXIT.OK : EXIT.FAILURE;
|
|
919
930
|
}
|
|
920
|
-
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances daily [show|on --hour 7 --to a@x|off|send-now]');
|
|
931
|
+
throw new CliExit(EXIT.INVALID, 'Usage: coinpay finances daily [show|on --hour 7 --to a@x|off|send-now] [--target 100|--target clear]');
|
|
921
932
|
}
|
|
922
933
|
|
|
923
934
|
case 'payloads': {
|
package/src/finances-reports.js
CHANGED
|
@@ -461,6 +461,12 @@ export async function getDailySummary(client) {
|
|
|
461
461
|
return data.schedule;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/** The daily summary schedule plus the owner's $/day spend target (null when none). */
|
|
465
|
+
export async function getDailySummaryWithTarget(client) {
|
|
466
|
+
const data = await call(client, '/finances/email-schedules?kind=daily_summary');
|
|
467
|
+
return { schedule: data.schedule, target: data.dailySpendTarget ?? null };
|
|
468
|
+
}
|
|
469
|
+
|
|
464
470
|
export async function setDailySummary(client, { weekdays, hour, timezone, recipients, active } = {}) {
|
|
465
471
|
const data = await call(client, '/finances/email-schedules', {
|
|
466
472
|
method: 'POST',
|
|
@@ -469,6 +475,15 @@ export async function setDailySummary(client, { weekdays, hour, timezone, recipi
|
|
|
469
475
|
return data.schedule;
|
|
470
476
|
}
|
|
471
477
|
|
|
478
|
+
/** Set the daily summary's $/day spend target, or clear it with null. Does not turn the summary on. */
|
|
479
|
+
export async function setDailySpendTarget(client, target) {
|
|
480
|
+
const data = await call(client, '/finances/email-schedules', {
|
|
481
|
+
method: 'POST',
|
|
482
|
+
body: JSON.stringify({ kind: 'daily_summary', target }),
|
|
483
|
+
});
|
|
484
|
+
return { schedule: data.schedule, target: data.dailySpendTarget ?? null };
|
|
485
|
+
}
|
|
486
|
+
|
|
472
487
|
export async function deleteDailySummary(client) {
|
|
473
488
|
return call(client, '/finances/email-schedules?kind=daily_summary', { method: 'DELETE' });
|
|
474
489
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -93,6 +93,27 @@ export {
|
|
|
93
93
|
invoiceProjectTime,
|
|
94
94
|
} from './projects.js';
|
|
95
95
|
export type { ArAging, AgingRow, InvoiceSettings, Project, ProjectStats, TimeEntry } from './projects.js';
|
|
96
|
+
export {
|
|
97
|
+
listContractors,
|
|
98
|
+
getContractor,
|
|
99
|
+
addContractor,
|
|
100
|
+
createContractorPortalLink,
|
|
101
|
+
offboardContractor,
|
|
102
|
+
createContract,
|
|
103
|
+
sendContract,
|
|
104
|
+
endContract,
|
|
105
|
+
listWorkItems,
|
|
106
|
+
reviewWorkItem,
|
|
107
|
+
addContractorPayment,
|
|
108
|
+
listPayRuns,
|
|
109
|
+
getPayRun,
|
|
110
|
+
createPayRun,
|
|
111
|
+
deletePayRun,
|
|
112
|
+
payPayRun,
|
|
113
|
+
recordPayRunPayments,
|
|
114
|
+
get1099Summary,
|
|
115
|
+
} from './contractors.js';
|
|
116
|
+
export type { Contractor, ContractorContract, ContractorMilestone, ContractorWorkItem, PayRun, PayRunItem, PayOutcome } from './contractors.js';
|
|
96
117
|
|
|
97
118
|
// Webhook exports
|
|
98
119
|
export {
|
package/src/index.js
CHANGED
|
@@ -107,6 +107,27 @@ import {
|
|
|
107
107
|
invoiceProjectTime,
|
|
108
108
|
} from './projects.js';
|
|
109
109
|
|
|
110
|
+
import {
|
|
111
|
+
listContractors,
|
|
112
|
+
getContractor,
|
|
113
|
+
addContractor,
|
|
114
|
+
createContractorPortalLink,
|
|
115
|
+
offboardContractor,
|
|
116
|
+
createContract,
|
|
117
|
+
sendContract,
|
|
118
|
+
endContract,
|
|
119
|
+
listWorkItems,
|
|
120
|
+
reviewWorkItem,
|
|
121
|
+
addContractorPayment,
|
|
122
|
+
listPayRuns,
|
|
123
|
+
getPayRun,
|
|
124
|
+
createPayRun,
|
|
125
|
+
deletePayRun,
|
|
126
|
+
payPayRun,
|
|
127
|
+
recordPayRunPayments,
|
|
128
|
+
get1099Summary,
|
|
129
|
+
} from './contractors.js';
|
|
130
|
+
|
|
110
131
|
// Wallet exports
|
|
111
132
|
import {
|
|
112
133
|
WalletClient,
|
|
@@ -273,6 +294,24 @@ export {
|
|
|
273
294
|
stopTimer,
|
|
274
295
|
listTimeEntries,
|
|
275
296
|
invoiceProjectTime,
|
|
297
|
+
listContractors,
|
|
298
|
+
getContractor,
|
|
299
|
+
addContractor,
|
|
300
|
+
createContractorPortalLink,
|
|
301
|
+
offboardContractor,
|
|
302
|
+
createContract,
|
|
303
|
+
sendContract,
|
|
304
|
+
endContract,
|
|
305
|
+
listWorkItems,
|
|
306
|
+
reviewWorkItem,
|
|
307
|
+
addContractorPayment,
|
|
308
|
+
listPayRuns,
|
|
309
|
+
getPayRun,
|
|
310
|
+
createPayRun,
|
|
311
|
+
deletePayRun,
|
|
312
|
+
payPayRun,
|
|
313
|
+
recordPayRunPayments,
|
|
314
|
+
get1099Summary,
|
|
276
315
|
|
|
277
316
|
// Wallet
|
|
278
317
|
WalletClient,
|
package/src/mcp.js
CHANGED
|
@@ -17,6 +17,7 @@ import { getFinanceSummary, listFinanceAccounts, listFinanceTransactions } from
|
|
|
17
17
|
import * as sf from './statements-fetch.js';
|
|
18
18
|
import { listInvoices, getInvoice, createInvoice } from './invoices.js';
|
|
19
19
|
import * as projects from './projects.js';
|
|
20
|
+
import * as contractors from './contractors.js';
|
|
20
21
|
|
|
21
22
|
export const PROTOCOL_VERSION = '2025-06-18';
|
|
22
23
|
|
|
@@ -94,13 +95,14 @@ export const TOOLS = [
|
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
97
|
name: 'finance_daily_summary',
|
|
97
|
-
description: 'The daily summary email (due soon, new activity, position): show it, turn it on (optionally hour 0-23 and recipients), turn it off,
|
|
98
|
+
description: 'The daily summary email (spend vs a $/day target, top cuts, due soon, new activity, position): show it, turn it on (optionally hour 0-23 and recipients), turn it off, send one now, or set_target (target in $/day, null clears).',
|
|
98
99
|
inputSchema: {
|
|
99
100
|
type: 'object',
|
|
100
101
|
properties: {
|
|
101
|
-
action: { type: 'string', enum: ['show', 'on', 'off', 'send_now'] },
|
|
102
|
+
action: { type: 'string', enum: ['show', 'on', 'off', 'send_now', 'set_target'] },
|
|
102
103
|
hour: { type: 'integer', minimum: 0, maximum: 23 },
|
|
103
104
|
to: { type: 'array', items: { type: 'string' }, maxItems: 5 },
|
|
105
|
+
target: { type: ['number', 'null'], minimum: 0 },
|
|
104
106
|
},
|
|
105
107
|
required: ['action'],
|
|
106
108
|
additionalProperties: false,
|
|
@@ -109,7 +111,8 @@ export const TOOLS = [
|
|
|
109
111
|
if (args.action === 'on') return text({ schedule: await reports.setDailySummary(client, { hour: args.hour, recipients: args.to, active: true }) });
|
|
110
112
|
if (args.action === 'off') return text(await reports.deleteDailySummary(client));
|
|
111
113
|
if (args.action === 'send_now') return text(await reports.sendDailySummaryNow(client));
|
|
112
|
-
return text(
|
|
114
|
+
if (args.action === 'set_target') return text(await reports.setDailySpendTarget(client, args.target ?? null));
|
|
115
|
+
return text(await reports.getDailySummaryWithTarget(client));
|
|
113
116
|
},
|
|
114
117
|
},
|
|
115
118
|
{
|
|
@@ -321,6 +324,68 @@ export const TOOLS = [
|
|
|
321
324
|
inputSchema: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'], additionalProperties: false },
|
|
322
325
|
handler: async (args, { client }) => text(await projects.invoiceProjectTime(client, args.projectId)),
|
|
323
326
|
},
|
|
327
|
+
// Contractors. None of these sends money: paying a run is a signed-in owner's act in the dashboard or CLI.
|
|
328
|
+
{
|
|
329
|
+
name: 'contractor_list',
|
|
330
|
+
description: 'List contractors with wallet and tax-form status and how many submissions wait for approval.',
|
|
331
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, includeOffboarded: { type: 'boolean' } }, additionalProperties: false },
|
|
332
|
+
handler: async (args, { client }) => text({ contractors: await contractors.listContractors(client, args) }),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'contractor_get',
|
|
336
|
+
description: 'One contractor: contracts and milestones, work lines and payments.',
|
|
337
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
338
|
+
handler: async (args, { client }) => text(await contractors.getContractor(client, args.id)),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'contractor_work_list',
|
|
342
|
+
description: 'Contractor work lines (hours, milestones, expenses, bonuses, fixed fees). Use status "submitted" for what waits for approval.',
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: {
|
|
346
|
+
businessId: { type: 'string' },
|
|
347
|
+
status: { type: 'string', enum: ['submitted', 'approved', 'rejected', 'paid'] },
|
|
348
|
+
contractorId: { type: 'string' },
|
|
349
|
+
},
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
},
|
|
352
|
+
handler: async (args, { client }) => text({ work_items: await contractors.listWorkItems(client, args) }),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: 'contractor_work_review',
|
|
356
|
+
description: 'Approve or reject a submitted work line. A rejection needs a note the contractor will see. Approving does not pay; it queues the line for the next pay run.',
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: { id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject'] }, note: { type: 'string' } },
|
|
360
|
+
required: ['id', 'action'],
|
|
361
|
+
additionalProperties: false,
|
|
362
|
+
},
|
|
363
|
+
handler: async (args, { client }) => text({ work_item: await contractors.reviewWorkItem(client, args.id, args) }),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'pay_run_list',
|
|
367
|
+
description: 'Pay runs with totals and how many contractors are paid.',
|
|
368
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' } }, additionalProperties: false },
|
|
369
|
+
handler: async (args, { client }) => text({ pay_runs: await contractors.listPayRuns(client, args) }),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'pay_run_get',
|
|
373
|
+
description: 'One pay run: per-contractor amounts, lines, status and warnings (missing or recently changed wallets).',
|
|
374
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
375
|
+
handler: async (args, { client }) => text(await contractors.getPayRun(client, args.id)),
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'pay_run_draft',
|
|
379
|
+
description: 'Draft a pay run from fixed fees due by payDate (YYYY-MM-DD, default today) plus all approved work. Sends nothing; the owner reviews and pays it.',
|
|
380
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, payDate: { type: 'string' } }, additionalProperties: false },
|
|
381
|
+
handler: async (args, { client }) => text({ pay_run: await contractors.createPayRun(client, args) }),
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'contractor_1099_summary',
|
|
385
|
+
description: 'Totals paid per contractor in a calendar year and who needs a 1099-NEC (US, $600+, not a corporation). Shows only the last four of tax ids.',
|
|
386
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, year: { type: 'integer' } }, additionalProperties: false },
|
|
387
|
+
handler: async (args, { client }) => text(await contractors.get1099Summary(client, args)),
|
|
388
|
+
},
|
|
324
389
|
];
|
|
325
390
|
|
|
326
391
|
/** Answer one JSON-RPC message; null for a notification. */
|