@profullstack/coinpay 0.17.0 → 0.19.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 +40 -0
- package/bin/coinpay.js +509 -2
- package/package.json +1 -1
- package/src/contractors.d.ts +124 -0
- package/src/contractors.js +135 -0
- package/src/index.d.ts +39 -0
- package/src/index.js +69 -0
- package/src/mcp.js +166 -0
- package/src/projects.d.ts +91 -0
- package/src/projects.js +106 -0
|
@@ -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/index.d.ts
CHANGED
|
@@ -76,6 +76,45 @@ export type {
|
|
|
76
76
|
ListInvoicesResult,
|
|
77
77
|
} from './invoices.js';
|
|
78
78
|
|
|
79
|
+
// Projects, time, aging, invoice settings, portal links
|
|
80
|
+
export {
|
|
81
|
+
getArAging,
|
|
82
|
+
getInvoiceSettings,
|
|
83
|
+
updateInvoiceSettings,
|
|
84
|
+
createClientPortalLink,
|
|
85
|
+
revokeClientPortalLink,
|
|
86
|
+
listProjects,
|
|
87
|
+
getProject,
|
|
88
|
+
createProject,
|
|
89
|
+
logTime,
|
|
90
|
+
startTimer,
|
|
91
|
+
stopTimer,
|
|
92
|
+
listTimeEntries,
|
|
93
|
+
invoiceProjectTime,
|
|
94
|
+
} from './projects.js';
|
|
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';
|
|
117
|
+
|
|
79
118
|
// Webhook exports
|
|
80
119
|
export {
|
|
81
120
|
verifyWebhookSignature,
|
package/src/index.js
CHANGED
|
@@ -90,6 +90,44 @@ import {
|
|
|
90
90
|
deleteInvoiceItem,
|
|
91
91
|
} from './invoices.js';
|
|
92
92
|
|
|
93
|
+
// Projects, time, aging, invoice settings, portal links
|
|
94
|
+
import {
|
|
95
|
+
getArAging,
|
|
96
|
+
getInvoiceSettings,
|
|
97
|
+
updateInvoiceSettings,
|
|
98
|
+
createClientPortalLink,
|
|
99
|
+
revokeClientPortalLink,
|
|
100
|
+
listProjects,
|
|
101
|
+
getProject,
|
|
102
|
+
createProject,
|
|
103
|
+
logTime,
|
|
104
|
+
startTimer,
|
|
105
|
+
stopTimer,
|
|
106
|
+
listTimeEntries,
|
|
107
|
+
invoiceProjectTime,
|
|
108
|
+
} from './projects.js';
|
|
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
|
+
|
|
93
131
|
// Wallet exports
|
|
94
132
|
import {
|
|
95
133
|
WalletClient,
|
|
@@ -243,6 +281,37 @@ export {
|
|
|
243
281
|
listInvoiceItems,
|
|
244
282
|
createInvoiceItem,
|
|
245
283
|
deleteInvoiceItem,
|
|
284
|
+
getArAging,
|
|
285
|
+
getInvoiceSettings,
|
|
286
|
+
updateInvoiceSettings,
|
|
287
|
+
createClientPortalLink,
|
|
288
|
+
revokeClientPortalLink,
|
|
289
|
+
listProjects,
|
|
290
|
+
getProject,
|
|
291
|
+
createProject,
|
|
292
|
+
logTime,
|
|
293
|
+
startTimer,
|
|
294
|
+
stopTimer,
|
|
295
|
+
listTimeEntries,
|
|
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,
|
|
246
315
|
|
|
247
316
|
// Wallet
|
|
248
317
|
WalletClient,
|
package/src/mcp.js
CHANGED
|
@@ -15,6 +15,9 @@ import { createInterface } from 'node:readline';
|
|
|
15
15
|
import * as reports from './finances-reports.js';
|
|
16
16
|
import { getFinanceSummary, listFinanceAccounts, listFinanceTransactions } from './finances.js';
|
|
17
17
|
import * as sf from './statements-fetch.js';
|
|
18
|
+
import { listInvoices, getInvoice, createInvoice } from './invoices.js';
|
|
19
|
+
import * as projects from './projects.js';
|
|
20
|
+
import * as contractors from './contractors.js';
|
|
18
21
|
|
|
19
22
|
export const PROTOCOL_VERSION = '2025-06-18';
|
|
20
23
|
|
|
@@ -218,6 +221,169 @@ export const TOOLS = [
|
|
|
218
221
|
return { ...text({ banks: results }), isError: false };
|
|
219
222
|
},
|
|
220
223
|
},
|
|
224
|
+
{
|
|
225
|
+
name: 'invoice_list',
|
|
226
|
+
description: 'List invoices (number, status, amount, currency, due date, client). Filter by status: draft, sent, overdue, paid, cancelled.',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
businessId: { type: 'string' },
|
|
231
|
+
status: { type: 'string', enum: ['draft', 'sent', 'overdue', 'paid', 'cancelled'] },
|
|
232
|
+
clientId: { type: 'string' },
|
|
233
|
+
},
|
|
234
|
+
additionalProperties: false,
|
|
235
|
+
},
|
|
236
|
+
handler: async (args, { client }) => text(await listInvoices(client, args)),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'invoice_get',
|
|
240
|
+
description: 'One invoice with its line items, totals, payment details and status.',
|
|
241
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
242
|
+
handler: async (args, { client }) => text(await getInvoice(client, args.id)),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'invoice_create_draft',
|
|
246
|
+
description:
|
|
247
|
+
'Create a DRAFT itemised invoice (nothing is sent). lineItems: [{description, quantity, unit_price, tax_rate?, tax_name?}]; discount: {type: percent|fixed, value}. The server computes the total.',
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
businessId: { type: 'string' },
|
|
252
|
+
clientId: { type: 'string' },
|
|
253
|
+
currency: { type: 'string', description: 'ISO code, default USD' },
|
|
254
|
+
dueDate: { type: 'string', description: 'ISO date' },
|
|
255
|
+
notes: { type: 'string' },
|
|
256
|
+
lineItems: {
|
|
257
|
+
type: 'array',
|
|
258
|
+
minItems: 1,
|
|
259
|
+
maxItems: 100,
|
|
260
|
+
items: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
description: { type: 'string' },
|
|
264
|
+
quantity: { type: 'number' },
|
|
265
|
+
unit_price: { type: 'number' },
|
|
266
|
+
tax_rate: { type: 'number' },
|
|
267
|
+
tax_name: { type: 'string' },
|
|
268
|
+
},
|
|
269
|
+
required: ['description', 'quantity', 'unit_price'],
|
|
270
|
+
additionalProperties: false,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
discount: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: { type: { type: 'string', enum: ['percent', 'fixed'] }, value: { type: 'number' } },
|
|
276
|
+
required: ['type', 'value'],
|
|
277
|
+
additionalProperties: false,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ['lineItems'],
|
|
281
|
+
additionalProperties: false,
|
|
282
|
+
},
|
|
283
|
+
handler: async (args, { client }) => text(await createInvoice(client, { ...args, currency: args.currency || 'USD' })),
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'invoice_ar_aging',
|
|
287
|
+
description: 'Accounts receivable aging: unpaid sent invoices by days past due (current, 1-30, 31-60, 61-90, 90+), per currency and per client.',
|
|
288
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' } }, additionalProperties: false },
|
|
289
|
+
handler: async (args, { client }) => text(await projects.getArAging(client, args)),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'project_list',
|
|
293
|
+
description: 'List active projects (name, client, hourly rate, budget).',
|
|
294
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, includeArchived: { type: 'boolean' } }, additionalProperties: false },
|
|
295
|
+
handler: async (args, { client }) => text({ projects: await projects.listProjects(client, args) }),
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'project_get',
|
|
299
|
+
description: 'One project with hours logged, unbilled value, billed and paid totals.',
|
|
300
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
301
|
+
handler: async (args, { client }) => text(await projects.getProject(client, args.id)),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: 'time_log',
|
|
305
|
+
description: 'Log finished time on a project, in minutes.',
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
projectId: { type: 'string' },
|
|
310
|
+
minutes: { type: 'integer', minimum: 1, maximum: 100000 },
|
|
311
|
+
description: { type: 'string' },
|
|
312
|
+
billable: { type: 'boolean' },
|
|
313
|
+
},
|
|
314
|
+
required: ['projectId', 'minutes'],
|
|
315
|
+
additionalProperties: false,
|
|
316
|
+
},
|
|
317
|
+
handler: async (args, { client }) => text({ time_entry: await projects.logTime(client, args) }),
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'project_invoice_time',
|
|
321
|
+
description: "Bill a project's unbilled billable time as a DRAFT itemised invoice (not sent). Returns the invoice number and amount.",
|
|
322
|
+
inputSchema: { type: 'object', properties: { projectId: { type: 'string' } }, required: ['projectId'], additionalProperties: false },
|
|
323
|
+
handler: async (args, { client }) => text(await projects.invoiceProjectTime(client, args.projectId)),
|
|
324
|
+
},
|
|
325
|
+
// Contractors. None of these sends money: paying a run is a signed-in owner's act in the dashboard or CLI.
|
|
326
|
+
{
|
|
327
|
+
name: 'contractor_list',
|
|
328
|
+
description: 'List contractors with wallet and tax-form status and how many submissions wait for approval.',
|
|
329
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, includeOffboarded: { type: 'boolean' } }, additionalProperties: false },
|
|
330
|
+
handler: async (args, { client }) => text({ contractors: await contractors.listContractors(client, args) }),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: 'contractor_get',
|
|
334
|
+
description: 'One contractor: contracts and milestones, work lines and payments.',
|
|
335
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
336
|
+
handler: async (args, { client }) => text(await contractors.getContractor(client, args.id)),
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'contractor_work_list',
|
|
340
|
+
description: 'Contractor work lines (hours, milestones, expenses, bonuses, fixed fees). Use status "submitted" for what waits for approval.',
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: 'object',
|
|
343
|
+
properties: {
|
|
344
|
+
businessId: { type: 'string' },
|
|
345
|
+
status: { type: 'string', enum: ['submitted', 'approved', 'rejected', 'paid'] },
|
|
346
|
+
contractorId: { type: 'string' },
|
|
347
|
+
},
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
},
|
|
350
|
+
handler: async (args, { client }) => text({ work_items: await contractors.listWorkItems(client, args) }),
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'contractor_work_review',
|
|
354
|
+
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.',
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: { id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject'] }, note: { type: 'string' } },
|
|
358
|
+
required: ['id', 'action'],
|
|
359
|
+
additionalProperties: false,
|
|
360
|
+
},
|
|
361
|
+
handler: async (args, { client }) => text({ work_item: await contractors.reviewWorkItem(client, args.id, args) }),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'pay_run_list',
|
|
365
|
+
description: 'Pay runs with totals and how many contractors are paid.',
|
|
366
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' } }, additionalProperties: false },
|
|
367
|
+
handler: async (args, { client }) => text({ pay_runs: await contractors.listPayRuns(client, args) }),
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'pay_run_get',
|
|
371
|
+
description: 'One pay run: per-contractor amounts, lines, status and warnings (missing or recently changed wallets).',
|
|
372
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
373
|
+
handler: async (args, { client }) => text(await contractors.getPayRun(client, args.id)),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: 'pay_run_draft',
|
|
377
|
+
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.',
|
|
378
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, payDate: { type: 'string' } }, additionalProperties: false },
|
|
379
|
+
handler: async (args, { client }) => text({ pay_run: await contractors.createPayRun(client, args) }),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'contractor_1099_summary',
|
|
383
|
+
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.',
|
|
384
|
+
inputSchema: { type: 'object', properties: { businessId: { type: 'string' }, year: { type: 'integer' } }, additionalProperties: false },
|
|
385
|
+
handler: async (args, { client }) => text(await contractors.get1099Summary(client, args)),
|
|
386
|
+
},
|
|
221
387
|
];
|
|
222
388
|
|
|
223
389
|
/** Answer one JSON-RPC message; null for a notification. */
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { CoinPayClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
export interface AgingRow {
|
|
4
|
+
currency: string;
|
|
5
|
+
current: number;
|
|
6
|
+
'1_30': number;
|
|
7
|
+
'31_60': number;
|
|
8
|
+
'61_90': number;
|
|
9
|
+
'90_plus': number;
|
|
10
|
+
total: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ArAging {
|
|
14
|
+
as_of: string;
|
|
15
|
+
totals: AgingRow[];
|
|
16
|
+
clients: Array<AgingRow & { client_id: string | null; name: string; invoices: number }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InvoiceSettings {
|
|
20
|
+
business_id: string;
|
|
21
|
+
reminders_enabled: boolean;
|
|
22
|
+
reminder_days: number[];
|
|
23
|
+
late_fee_type: 'none' | 'percent' | 'fixed';
|
|
24
|
+
late_fee_value: number | null;
|
|
25
|
+
late_fee_after_days: number;
|
|
26
|
+
brand_color: string | null;
|
|
27
|
+
logo_url: string | null;
|
|
28
|
+
invoice_footer: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Project {
|
|
32
|
+
id: string;
|
|
33
|
+
business_id: string;
|
|
34
|
+
client_id: string | null;
|
|
35
|
+
name: string;
|
|
36
|
+
hourly_rate: number | null;
|
|
37
|
+
currency: string;
|
|
38
|
+
budget_hours: number | null;
|
|
39
|
+
status: 'active' | 'archived';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProjectStats {
|
|
43
|
+
hours: number;
|
|
44
|
+
billable_hours: number;
|
|
45
|
+
unbilled_amount: number;
|
|
46
|
+
billed_amount: number;
|
|
47
|
+
paid_amount: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TimeEntry {
|
|
51
|
+
id: string;
|
|
52
|
+
project_id: string;
|
|
53
|
+
description: string;
|
|
54
|
+
started_at: string;
|
|
55
|
+
ended_at: string | null;
|
|
56
|
+
/** null while the timer is running */
|
|
57
|
+
minutes: number | null;
|
|
58
|
+
billable: boolean;
|
|
59
|
+
hourly_rate: number | null;
|
|
60
|
+
invoice_id: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getArAging(client: CoinPayClient, options?: { businessId?: string }): Promise<ArAging>;
|
|
64
|
+
export function getInvoiceSettings(client: CoinPayClient, options?: { businessId?: string }): Promise<InvoiceSettings>;
|
|
65
|
+
export function updateInvoiceSettings(
|
|
66
|
+
client: CoinPayClient,
|
|
67
|
+
options: { businessId?: string } & Partial<Omit<InvoiceSettings, 'business_id'>>
|
|
68
|
+
): Promise<InvoiceSettings>;
|
|
69
|
+
export function createClientPortalLink(client: CoinPayClient, clientId: string): Promise<string>;
|
|
70
|
+
export function revokeClientPortalLink(client: CoinPayClient, clientId: string): Promise<Record<string, unknown>>;
|
|
71
|
+
export function listProjects(client: CoinPayClient, options?: { businessId?: string; includeArchived?: boolean }): Promise<Project[]>;
|
|
72
|
+
export function getProject(client: CoinPayClient, id: string): Promise<{ project: Project; stats: ProjectStats }>;
|
|
73
|
+
export function createProject(
|
|
74
|
+
client: CoinPayClient,
|
|
75
|
+
options: { businessId?: string; name: string; clientId?: string; hourlyRate?: number; currency?: string; budgetHours?: number; notes?: string }
|
|
76
|
+
): Promise<Project>;
|
|
77
|
+
export function logTime(
|
|
78
|
+
client: CoinPayClient,
|
|
79
|
+
options: { projectId: string; minutes: number; description?: string; billable?: boolean; hourlyRate?: number; startedAt?: string }
|
|
80
|
+
): Promise<TimeEntry>;
|
|
81
|
+
export function startTimer(client: CoinPayClient, options: { projectId: string; description?: string }): Promise<TimeEntry>;
|
|
82
|
+
export function stopTimer(client: CoinPayClient, options?: { entryId?: string; businessId?: string }): Promise<TimeEntry>;
|
|
83
|
+
export function listTimeEntries(
|
|
84
|
+
client: CoinPayClient,
|
|
85
|
+
options?: { projectId?: string; businessId?: string; unbilled?: boolean }
|
|
86
|
+
): Promise<TimeEntry[]>;
|
|
87
|
+
export function invoiceProjectTime(
|
|
88
|
+
client: CoinPayClient,
|
|
89
|
+
projectId: string,
|
|
90
|
+
options?: { entryIds?: string[] }
|
|
91
|
+
): Promise<{ invoice: { id: string; invoice_number: string; amount: number }; entries_billed: number }>;
|