@go-labs-sg/bb 1.13.0 → 1.15.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 +10 -3
- package/dist/commands.js +262 -3
- package/dist/index.js +107 -4
- package/dist/parse-cli-enums.js +2 -2
- package/dist/prisma-enums.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ For AI agents: bills and claims are stored as the same underlying bill records.
|
|
|
6
6
|
|
|
7
7
|
For AI agents using this CLI: always check whether a newer `@go-labs-sg/bb` version is available before running Budget Builder commands, then update the CLI if needed so command flags and API behavior stay current.
|
|
8
8
|
|
|
9
|
+
For AI agents using this CLI: do not execute approval decisions, approval requests, status updates, `mark-budget-won`, `submit-quotation`, or supplier approval request side effects until the user explicitly confirms the exact entity, target state, and side effects. Inspect the entity first, summarize what will change and whether emails/integrations will run, then wait for a clear confirmation from the user. These sensitive workflow commands also require an interactive `CONFIRM` prompt at runtime and abort in non-interactive shells.
|
|
10
|
+
|
|
9
11
|
**Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
|
|
10
12
|
|
|
11
13
|
## Requirements
|
|
@@ -56,6 +58,10 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
56
58
|
|
|
57
59
|
**Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. If the parent project is `PITCH` or `LOST`, the API now marks it `WON` automatically and, when the `asana-tasks` feature flag is enabled, creates the Asana project/section/tasks automatically. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
|
|
58
60
|
|
|
61
|
+
**Sensitive workflow changes:** Approval decisions/requests, entity status updates, `mark-budget-won`, `submit-quotation`, and supplier approval request side effects require an interactive `CONFIRM` prompt. Non-interactive runs abort before the guarded workflow mutation. Agents must get user confirmation in chat before attempting the command; the prompt is a final runtime guard, not a replacement for user approval.
|
|
62
|
+
|
|
63
|
+
**Locked estimate budget changes:** Budget-changing CLI commands require an interactive `yes` confirmation when the current budget status is `ESTIMATE_CREATED`, `ESTIMATE_SENT`, `ESTIMATE_ACCEPTED`, `ESTIMATE_REJECTED`, or `ESTIMATE_CLOSED`. There is no force/override flag; non-interactive runs abort before mutating locked budgets.
|
|
64
|
+
|
|
59
65
|
**Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. Plain `description` fields for item create/update are converted to `descriptionRichText`; pass `descriptionRichText` directly when formatted Tiptap JSON is required. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
|
|
60
66
|
|
|
61
67
|
### Command overview
|
|
@@ -63,9 +69,10 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
63
69
|
| Area | Commands (non-exhaustive) |
|
|
64
70
|
| --- | --- |
|
|
65
71
|
| **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates Asana project/section/tasks when enabled; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
66
|
-
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
67
|
-
| **
|
|
68
|
-
| **
|
|
72
|
+
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
73
|
+
| **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
74
|
+
| **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation` |
|
|
75
|
+
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; optional `--pax`; optional `--endDate`; optional `--description`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
|
|
69
76
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
70
77
|
| **Suppliers & items** | `list-suppliers` (defaults to active suppliers, `--perPage 10`, sorted by `createdAt` desc; supports `--name`, `--sortBy` for scalar supplier fields, `--sortDir`, `--createdBy`, `--gstRegistered`, `--status`, `--supplierTags`, `--active false` for archived suppliers), `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin; archives/deactivates related items), `reactivate-suppliers` (`--ids` CSV; admin; reactivates related items), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details` (includes `supplierApprovalSummary` for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
|
|
71
78
|
| **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
package/dist/commands.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { basename } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
3
5
|
import { contentType } from "mime-types";
|
|
4
6
|
import { api } from "./api-client.js";
|
|
5
7
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
|
|
6
8
|
import { billStatusesForApi, } from "./parse-cli-enums.js";
|
|
7
9
|
import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, } from "./parse-mutation-payload.js";
|
|
10
|
+
import { BudgetStatus, } from "./prisma-enums.js";
|
|
8
11
|
import { createRichTextFromPlainText } from "./rich-text.js";
|
|
9
12
|
const BUDGET = "BUDGET";
|
|
10
13
|
const BILL = "BILL";
|
|
11
14
|
const SUPPLIER = "SUPPLIER";
|
|
15
|
+
const QUOTATION = "QUOTATION";
|
|
12
16
|
function isPendingRequest(n) {
|
|
13
17
|
return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
|
|
14
18
|
}
|
|
@@ -25,6 +29,82 @@ const contentTypeHeaderForFileName = (fileName) => {
|
|
|
25
29
|
const ct = contentType(fileName);
|
|
26
30
|
return typeof ct === "string" ? ct : "application/octet-stream";
|
|
27
31
|
};
|
|
32
|
+
const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
|
|
33
|
+
BudgetStatus.ESTIMATE_CREATED,
|
|
34
|
+
BudgetStatus.ESTIMATE_SENT,
|
|
35
|
+
BudgetStatus.ESTIMATE_ACCEPTED,
|
|
36
|
+
BudgetStatus.ESTIMATE_REJECTED,
|
|
37
|
+
BudgetStatus.ESTIMATE_CLOSED,
|
|
38
|
+
]);
|
|
39
|
+
const lockedBudgetStatusLabels = {
|
|
40
|
+
ESTIMATE_CREATED: "Estimate Created",
|
|
41
|
+
ESTIMATE_SENT: "Estimate Sent",
|
|
42
|
+
ESTIMATE_ACCEPTED: "Estimate Accepted",
|
|
43
|
+
ESTIMATE_REJECTED: "Estimate Rejected",
|
|
44
|
+
ESTIMATE_CLOSED: "Estimate Closed",
|
|
45
|
+
};
|
|
46
|
+
const lockedBudgetStatusLabel = (status) => status in lockedBudgetStatusLabels
|
|
47
|
+
? lockedBudgetStatusLabels[status]
|
|
48
|
+
: status;
|
|
49
|
+
const assertSensitiveWorkflowConfirmed = async ({ action, entity, details, }) => {
|
|
50
|
+
const expected = "CONFIRM";
|
|
51
|
+
const detailText = details ? ` ${details}` : "";
|
|
52
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
53
|
+
throw new Error(`${action} on ${entity}${detailText} is a sensitive Budget Builder workflow change. Run this command in an interactive terminal after the user explicitly confirms the exact action, then type "${expected}" to continue.`);
|
|
54
|
+
}
|
|
55
|
+
const rl = createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stderr,
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const answer = await rl.question(`${action} on ${entity}${detailText} may change workflow status, approvals, or send notification emails. Confirm the user explicitly approved this exact action, then type "${expected}" to continue: `);
|
|
61
|
+
if (answer.trim() !== expected) {
|
|
62
|
+
throw new Error("Aborted.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
rl.close();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const assertLockedBudgetChangeConfirmed = async ({ budget, action, }) => {
|
|
70
|
+
if (!LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION.has(budget.status)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const statusLabel = lockedBudgetStatusLabel(budget.status);
|
|
74
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
75
|
+
throw new Error(`${action} would change locked budget "${budget.name}" (${budget.id}) with status ${statusLabel}. Run this command in an interactive terminal and type "yes" to confirm.`);
|
|
76
|
+
}
|
|
77
|
+
const rl = createInterface({
|
|
78
|
+
input: process.stdin,
|
|
79
|
+
output: process.stderr,
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const answer = await rl.question(`${action} will change locked budget "${budget.name}" (${budget.id}) with status ${statusLabel}. Type "yes" to continue: `);
|
|
83
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
84
|
+
throw new Error("Aborted.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
rl.close();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const confirmLockedBudgetChangeByBudgetId = async (budgetId, action) => {
|
|
92
|
+
const budget = await api.budget.getBudget.query({ id: budgetId });
|
|
93
|
+
await assertLockedBudgetChangeConfirmed({
|
|
94
|
+
budget: {
|
|
95
|
+
id: budget.id,
|
|
96
|
+
name: budget.name,
|
|
97
|
+
status: budget.status,
|
|
98
|
+
},
|
|
99
|
+
action,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const confirmLockedBudgetChangeByBudgetItemId = async (budgetItemId, action) => {
|
|
103
|
+
const { budget } = await api.budgetItem.getBudgetItemContext.query({
|
|
104
|
+
budgetItemId,
|
|
105
|
+
});
|
|
106
|
+
await assertLockedBudgetChangeConfirmed({ budget, action });
|
|
107
|
+
};
|
|
28
108
|
export async function listBudgets(opts) {
|
|
29
109
|
const input = {
|
|
30
110
|
projectId: opts.projectId,
|
|
@@ -50,6 +130,12 @@ export async function getBudget(id) {
|
|
|
50
130
|
out({ budget, items });
|
|
51
131
|
}
|
|
52
132
|
export async function updateBudgetStatus(budgetId, status, opts) {
|
|
133
|
+
await assertSensitiveWorkflowConfirmed({
|
|
134
|
+
action: "Update budget status",
|
|
135
|
+
entity: `budget ${budgetId}`,
|
|
136
|
+
details: `to ${status}`,
|
|
137
|
+
});
|
|
138
|
+
await confirmLockedBudgetChangeByBudgetId(budgetId, "Update budget status");
|
|
53
139
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
54
140
|
id: budgetId,
|
|
55
141
|
status,
|
|
@@ -147,6 +233,11 @@ export async function listBills(opts) {
|
|
|
147
233
|
out(result);
|
|
148
234
|
}
|
|
149
235
|
export async function approveBill(billId) {
|
|
236
|
+
await assertSensitiveWorkflowConfirmed({
|
|
237
|
+
action: "Approve bill",
|
|
238
|
+
entity: `bill ${billId}`,
|
|
239
|
+
details: "and send requester reply email",
|
|
240
|
+
});
|
|
150
241
|
const notifications = await api.budget.getNotifications.query();
|
|
151
242
|
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
152
243
|
n.type === BILL &&
|
|
@@ -199,6 +290,10 @@ export async function listApprovals(opts) {
|
|
|
199
290
|
id = n.supplier.id;
|
|
200
291
|
name = n.supplier.name;
|
|
201
292
|
}
|
|
293
|
+
else if (n.type === QUOTATION && n.supplierQuotation) {
|
|
294
|
+
id = n.supplierQuotation.id;
|
|
295
|
+
name = `Quotation ${n.supplierQuotation.supplier.name}`;
|
|
296
|
+
}
|
|
202
297
|
else {
|
|
203
298
|
id = n.id;
|
|
204
299
|
name = "Unknown";
|
|
@@ -234,6 +329,11 @@ export async function listSuppliers(opts) {
|
|
|
234
329
|
}
|
|
235
330
|
// --- Bills (MCP parity) ---
|
|
236
331
|
export async function createBillApproval(billId) {
|
|
332
|
+
await assertSensitiveWorkflowConfirmed({
|
|
333
|
+
action: "Create bill approval request",
|
|
334
|
+
entity: `bill ${billId}`,
|
|
335
|
+
details: "and send approval request emails",
|
|
336
|
+
});
|
|
237
337
|
const result = await api.bill.createApproval.mutate({ billId });
|
|
238
338
|
const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
|
|
239
339
|
let email;
|
|
@@ -259,6 +359,11 @@ export async function createBillApproval(billId) {
|
|
|
259
359
|
});
|
|
260
360
|
}
|
|
261
361
|
export async function updateBillStatus(opts) {
|
|
362
|
+
await assertSensitiveWorkflowConfirmed({
|
|
363
|
+
action: "Update bill status",
|
|
364
|
+
entity: `bill ${opts.id}`,
|
|
365
|
+
details: `to ${opts.status}`,
|
|
366
|
+
});
|
|
262
367
|
const result = await api.bill.updateStatus.mutate({
|
|
263
368
|
id: opts.id,
|
|
264
369
|
status: opts.status,
|
|
@@ -323,7 +428,35 @@ export async function uploadBillAttachmentFromPath(billId, filePath) {
|
|
|
323
428
|
});
|
|
324
429
|
out(confirmed);
|
|
325
430
|
}
|
|
431
|
+
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
432
|
+
const buf = await readFile(filePath);
|
|
433
|
+
const fileName = basename(filePath);
|
|
434
|
+
const ext = fileName
|
|
435
|
+
.split(".")
|
|
436
|
+
.pop()
|
|
437
|
+
?.toLowerCase()
|
|
438
|
+
.replace(/[^a-z0-9]/g, "");
|
|
439
|
+
const key = `quotations/${projectId}/${randomUUID()}.${ext || "pdf"}`;
|
|
440
|
+
const uploadUrl = await api.attachment.getPresignedUrlToUpload.mutate({
|
|
441
|
+
key,
|
|
442
|
+
});
|
|
443
|
+
const res = await fetch(uploadUrl, {
|
|
444
|
+
method: "PUT",
|
|
445
|
+
body: buf,
|
|
446
|
+
headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
|
|
447
|
+
});
|
|
448
|
+
if (!res.ok) {
|
|
449
|
+
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
450
|
+
}
|
|
451
|
+
out({
|
|
452
|
+
id: randomUUID(),
|
|
453
|
+
name: fileName,
|
|
454
|
+
key,
|
|
455
|
+
size: buf.byteLength,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
326
458
|
export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
459
|
+
await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
|
|
327
460
|
const buf = await readFile(filePath);
|
|
328
461
|
const fileName = basename(filePath);
|
|
329
462
|
const size = buf.byteLength;
|
|
@@ -349,6 +482,12 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
|
349
482
|
out(confirmed);
|
|
350
483
|
}
|
|
351
484
|
export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
485
|
+
await assertSensitiveWorkflowConfirmed({
|
|
486
|
+
action: "Mark budget won",
|
|
487
|
+
entity: `budget ${budgetId}`,
|
|
488
|
+
details: "by uploading proof and setting status to ESTIMATE_ACCEPTED",
|
|
489
|
+
});
|
|
490
|
+
await confirmLockedBudgetChangeByBudgetId(budgetId, "Mark budget won");
|
|
352
491
|
const buf = await readFile(filePath);
|
|
353
492
|
const fileName = basename(filePath);
|
|
354
493
|
const size = buf.byteLength;
|
|
@@ -393,10 +532,12 @@ export async function getBudgetItemsOnly(budgetId) {
|
|
|
393
532
|
out(items);
|
|
394
533
|
}
|
|
395
534
|
export async function addBudgetItems(opts) {
|
|
535
|
+
await confirmLockedBudgetChangeByBudgetId(opts.budgetId, "Add budget items");
|
|
396
536
|
const result = await api.budgetItem.addBudgetItems.mutate(opts);
|
|
397
537
|
out(result);
|
|
398
538
|
}
|
|
399
539
|
export async function updateBudgetItem(input) {
|
|
540
|
+
await confirmLockedBudgetChangeByBudgetItemId(input.id, "Update budget item");
|
|
400
541
|
const result = await api.budgetItem.updateBudgetItem.mutate({
|
|
401
542
|
...input,
|
|
402
543
|
...(input.description !== undefined &&
|
|
@@ -411,6 +552,7 @@ export async function updateBudgetItem(input) {
|
|
|
411
552
|
out(result);
|
|
412
553
|
}
|
|
413
554
|
export async function removeBudgetItem(budgetItemId) {
|
|
555
|
+
await confirmLockedBudgetChangeByBudgetItemId(budgetItemId, "Remove budget item");
|
|
414
556
|
const result = await api.budgetItem.removeBudgetItem.mutate({
|
|
415
557
|
budgetItemId,
|
|
416
558
|
});
|
|
@@ -427,6 +569,12 @@ export async function getBudgetVersions(budgetId) {
|
|
|
427
569
|
out(versions);
|
|
428
570
|
}
|
|
429
571
|
export async function createBudgetApproval(budgetId, preflight) {
|
|
572
|
+
await assertSensitiveWorkflowConfirmed({
|
|
573
|
+
action: "Create budget approval request",
|
|
574
|
+
entity: `budget ${budgetId}`,
|
|
575
|
+
details: "and send approval request emails",
|
|
576
|
+
});
|
|
577
|
+
await confirmLockedBudgetChangeByBudgetId(budgetId, "Create budget approval");
|
|
430
578
|
await runBudgetPreflight(budgetId, preflight);
|
|
431
579
|
await assertBudgetActionReadiness({
|
|
432
580
|
budgetId,
|
|
@@ -458,6 +606,7 @@ export async function createBudgetApproval(budgetId, preflight) {
|
|
|
458
606
|
});
|
|
459
607
|
}
|
|
460
608
|
export async function createEstimate(budgetId, preflight) {
|
|
609
|
+
await confirmLockedBudgetChangeByBudgetId(budgetId, "Create estimate");
|
|
461
610
|
await runBudgetPreflight(budgetId, preflight);
|
|
462
611
|
await assertBudgetActionReadiness({
|
|
463
612
|
budgetId,
|
|
@@ -478,10 +627,12 @@ export async function createBudgetFromPayload(raw) {
|
|
|
478
627
|
}
|
|
479
628
|
export async function updateBudgetFromPayload(raw) {
|
|
480
629
|
const input = parseUpdateBudgetPayload(raw);
|
|
630
|
+
await confirmLockedBudgetChangeByBudgetId(input.id, "Update budget");
|
|
481
631
|
const result = await api.budget.updateBudget.mutate(input);
|
|
482
632
|
out(result);
|
|
483
633
|
}
|
|
484
634
|
export async function deleteBudgetById(budgetId) {
|
|
635
|
+
await confirmLockedBudgetChangeByBudgetId(budgetId, "Delete budget");
|
|
485
636
|
const result = await api.budget.deleteBudget.mutate({ id: budgetId });
|
|
486
637
|
out(result);
|
|
487
638
|
}
|
|
@@ -494,10 +645,12 @@ export async function deleteCompanyById(companyId) {
|
|
|
494
645
|
out(result);
|
|
495
646
|
}
|
|
496
647
|
export async function reorderBudgetItemsCli(opts) {
|
|
648
|
+
await confirmLockedBudgetChangeByBudgetId(opts.budgetId, "Reorder budget items");
|
|
497
649
|
const result = await api.budgetItem.reorderBudgetItems.mutate(opts);
|
|
498
650
|
out(result);
|
|
499
651
|
}
|
|
500
652
|
export async function updateBudgetItemSupplierCli(opts) {
|
|
653
|
+
await confirmLockedBudgetChangeByBudgetItemId(opts.budgetItemId, "Update budget item supplier");
|
|
501
654
|
const result = await api.budgetItem.updateBudgetItemSupplier.mutate(opts);
|
|
502
655
|
out(result);
|
|
503
656
|
}
|
|
@@ -515,11 +668,13 @@ export async function deleteBudgetCategory(id) {
|
|
|
515
668
|
}
|
|
516
669
|
export async function updateBudgetCommissionFromPayload(raw) {
|
|
517
670
|
const input = parseUpdateBudgetCommissionPayload(raw);
|
|
671
|
+
await confirmLockedBudgetChangeByBudgetId(input.budgetId, "Update budget commission");
|
|
518
672
|
const result = await api.budget.updateBudgetCommission.mutate(input);
|
|
519
673
|
out(result);
|
|
520
674
|
}
|
|
521
675
|
export async function updateBudgetDiscountFromPayload(raw) {
|
|
522
676
|
const input = parseBudgetDiscountPayload(raw);
|
|
677
|
+
await confirmLockedBudgetChangeByBudgetId(input.budgetId, "Update budget discount");
|
|
523
678
|
const result = await api.budget.updateBudgetDiscount.mutate(input);
|
|
524
679
|
out(result);
|
|
525
680
|
}
|
|
@@ -568,6 +723,33 @@ export async function createBillFromPayload(raw) {
|
|
|
568
723
|
const result = await api.bill.create.mutate(input);
|
|
569
724
|
out(result);
|
|
570
725
|
}
|
|
726
|
+
export async function listQuotations(opts) {
|
|
727
|
+
const result = await api.quotation.list.query(opts);
|
|
728
|
+
out(result);
|
|
729
|
+
}
|
|
730
|
+
export async function getQuotationDetails(id) {
|
|
731
|
+
const result = await api.quotation.getById.query({ id });
|
|
732
|
+
out(result);
|
|
733
|
+
}
|
|
734
|
+
export async function createQuotationFromPayload(raw) {
|
|
735
|
+
const result = await api.quotation.createDraft.mutate(raw);
|
|
736
|
+
out(result);
|
|
737
|
+
}
|
|
738
|
+
export async function submitQuotation(id) {
|
|
739
|
+
await assertSensitiveWorkflowConfirmed({
|
|
740
|
+
action: "Submit quotation for approval",
|
|
741
|
+
entity: `quotation ${id}`,
|
|
742
|
+
});
|
|
743
|
+
const result = await api.quotation.submitForApproval.mutate({ id });
|
|
744
|
+
out(result);
|
|
745
|
+
}
|
|
746
|
+
export async function downloadQuotationPdf(id, fileRole) {
|
|
747
|
+
const result = await api.quotation.getSignedDownloadUrl.query({
|
|
748
|
+
id,
|
|
749
|
+
fileRole,
|
|
750
|
+
});
|
|
751
|
+
out(result);
|
|
752
|
+
}
|
|
571
753
|
export async function updateBillFromPayload(raw) {
|
|
572
754
|
const input = parseUpdateBillPayload(raw);
|
|
573
755
|
const result = await api.bill.update.mutate(input);
|
|
@@ -585,6 +767,11 @@ export async function createSupplierFromPayload(raw) {
|
|
|
585
767
|
out(created);
|
|
586
768
|
return;
|
|
587
769
|
}
|
|
770
|
+
await assertSensitiveWorkflowConfirmed({
|
|
771
|
+
action: "Create supplier approval request",
|
|
772
|
+
entity: `supplier ${supplier.id}`,
|
|
773
|
+
details: "and send approval request emails",
|
|
774
|
+
});
|
|
588
775
|
const approval = await api.supplier.createSupplierApproval.mutate({
|
|
589
776
|
supplierId: supplier.id,
|
|
590
777
|
});
|
|
@@ -623,6 +810,11 @@ export async function updateSupplierFromPayload(raw) {
|
|
|
623
810
|
out(updated);
|
|
624
811
|
return;
|
|
625
812
|
}
|
|
813
|
+
await assertSensitiveWorkflowConfirmed({
|
|
814
|
+
action: "Create supplier approval request",
|
|
815
|
+
entity: `supplier ${supplier.id}`,
|
|
816
|
+
details: "and send approval request emails",
|
|
817
|
+
});
|
|
626
818
|
let approvalResult;
|
|
627
819
|
try {
|
|
628
820
|
const approval = await api.supplier.createSupplierApproval.mutate({
|
|
@@ -703,6 +895,11 @@ export async function updateCompanyFromPayload(raw) {
|
|
|
703
895
|
}
|
|
704
896
|
// --- Approvals: budgets / suppliers / bills (MCP parity) ---
|
|
705
897
|
export async function approveBudget(budgetId) {
|
|
898
|
+
await assertSensitiveWorkflowConfirmed({
|
|
899
|
+
action: "Approve budget",
|
|
900
|
+
entity: `budget ${budgetId}`,
|
|
901
|
+
details: "and send requester reply email",
|
|
902
|
+
});
|
|
706
903
|
const notifications = await api.budget.getNotifications.query();
|
|
707
904
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
708
905
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
@@ -734,6 +931,11 @@ export async function approveBudget(budgetId) {
|
|
|
734
931
|
});
|
|
735
932
|
}
|
|
736
933
|
export async function rejectBudget(budgetId, reason) {
|
|
934
|
+
await assertSensitiveWorkflowConfirmed({
|
|
935
|
+
action: "Reject budget",
|
|
936
|
+
entity: `budget ${budgetId}`,
|
|
937
|
+
details: `with reason "${reason}" and send requester reply email`,
|
|
938
|
+
});
|
|
737
939
|
const notifications = await api.budget.getNotifications.query();
|
|
738
940
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
739
941
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
@@ -827,6 +1029,11 @@ export async function getSupplierDetails(supplierId) {
|
|
|
827
1029
|
});
|
|
828
1030
|
}
|
|
829
1031
|
export async function approveSupplier(supplierId) {
|
|
1032
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1033
|
+
action: "Approve supplier",
|
|
1034
|
+
entity: `supplier ${supplierId}`,
|
|
1035
|
+
details: "and send requester reply email",
|
|
1036
|
+
});
|
|
830
1037
|
const notifications = await api.budget.getNotifications.query();
|
|
831
1038
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
832
1039
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
@@ -860,6 +1067,11 @@ export async function approveSupplier(supplierId) {
|
|
|
860
1067
|
});
|
|
861
1068
|
}
|
|
862
1069
|
export async function rejectSupplier(supplierId, reason) {
|
|
1070
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1071
|
+
action: "Reject supplier",
|
|
1072
|
+
entity: `supplier ${supplierId}`,
|
|
1073
|
+
details: `with reason "${reason}" and send requester reply email`,
|
|
1074
|
+
});
|
|
863
1075
|
const notifications = await api.budget.getNotifications.query();
|
|
864
1076
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
865
1077
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
@@ -894,6 +1106,11 @@ export async function rejectSupplier(supplierId, reason) {
|
|
|
894
1106
|
});
|
|
895
1107
|
}
|
|
896
1108
|
export async function rejectBill(billId, reason) {
|
|
1109
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1110
|
+
action: "Reject bill",
|
|
1111
|
+
entity: `bill ${billId}`,
|
|
1112
|
+
details: `with reason "${reason}" and send requester reply email`,
|
|
1113
|
+
});
|
|
897
1114
|
const notifications = await api.budget.getNotifications.query();
|
|
898
1115
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
899
1116
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
@@ -924,6 +1141,46 @@ export async function rejectBill(billId, reason) {
|
|
|
924
1141
|
email,
|
|
925
1142
|
});
|
|
926
1143
|
}
|
|
1144
|
+
export async function approveQuotation(quotationId) {
|
|
1145
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1146
|
+
action: "Approve quotation",
|
|
1147
|
+
entity: `quotation ${quotationId}`,
|
|
1148
|
+
});
|
|
1149
|
+
const notifications = await api.budget.getNotifications.query();
|
|
1150
|
+
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1151
|
+
n.type === QUOTATION &&
|
|
1152
|
+
(n.supplierQuotationId === quotationId ||
|
|
1153
|
+
n.supplierQuotation?.id === quotationId));
|
|
1154
|
+
const approval = pending[0];
|
|
1155
|
+
if (!approval) {
|
|
1156
|
+
throw new Error("No pending quotation approval found.");
|
|
1157
|
+
}
|
|
1158
|
+
const result = await api.quotation.approve.mutate({
|
|
1159
|
+
quotationApprovalId: approval.id,
|
|
1160
|
+
});
|
|
1161
|
+
out(result);
|
|
1162
|
+
}
|
|
1163
|
+
export async function rejectQuotation(quotationId, reason) {
|
|
1164
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1165
|
+
action: "Reject quotation",
|
|
1166
|
+
entity: `quotation ${quotationId}`,
|
|
1167
|
+
details: `with reason "${reason}"`,
|
|
1168
|
+
});
|
|
1169
|
+
const notifications = await api.budget.getNotifications.query();
|
|
1170
|
+
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1171
|
+
n.type === QUOTATION &&
|
|
1172
|
+
(n.supplierQuotationId === quotationId ||
|
|
1173
|
+
n.supplierQuotation?.id === quotationId));
|
|
1174
|
+
const approval = pending[0];
|
|
1175
|
+
if (!approval) {
|
|
1176
|
+
throw new Error("No pending quotation approval found.");
|
|
1177
|
+
}
|
|
1178
|
+
const result = await api.quotation.reject.mutate({
|
|
1179
|
+
quotationApprovalId: approval.id,
|
|
1180
|
+
rejectionReason: reason,
|
|
1181
|
+
});
|
|
1182
|
+
out(result);
|
|
1183
|
+
}
|
|
927
1184
|
// --- Companies & projects ---
|
|
928
1185
|
export async function listCompanies(opts) {
|
|
929
1186
|
const result = await api.company.getCompanies.query({
|
|
@@ -980,13 +1237,15 @@ export async function createProject(opts) {
|
|
|
980
1237
|
from: new Date(opts.startDate),
|
|
981
1238
|
to: opts.endDate ? new Date(opts.endDate) : undefined,
|
|
982
1239
|
},
|
|
983
|
-
...(opts.requestQboAccountantNotification !== undefined && {
|
|
984
|
-
requestQboAccountantNotification: opts.requestQboAccountantNotification,
|
|
985
|
-
}),
|
|
986
1240
|
});
|
|
987
1241
|
out(result);
|
|
988
1242
|
}
|
|
989
1243
|
export async function updateProjectStatus(id, status, opts) {
|
|
1244
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1245
|
+
action: "Update project status",
|
|
1246
|
+
entity: `project ${id}`,
|
|
1247
|
+
details: `to ${status}`,
|
|
1248
|
+
});
|
|
990
1249
|
const result = await api.project.updateProjectStatus.mutate({
|
|
991
1250
|
id,
|
|
992
1251
|
status,
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "./load-env.js";
|
|
3
3
|
import { requireApiKey } from "./api-client.js";
|
|
4
4
|
import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
|
|
5
|
-
import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
|
|
5
|
+
import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
|
|
6
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
7
|
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
8
8
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
@@ -119,6 +119,13 @@ Global: --quiet | -q | BB_CLI_QUIET=1 — hide action logs (default: log command
|
|
|
119
119
|
|
|
120
120
|
Auth: Set BB_API_KEY in the environment (same API key as MCP / Goracle).
|
|
121
121
|
|
|
122
|
+
Sensitive workflow changes: approval decisions/requests, entity status updates,
|
|
123
|
+
mark-budget-won, submit-quotation, and supplier approval request side effects
|
|
124
|
+
require an interactive CONFIRM prompt and abort in non-interactive shells.
|
|
125
|
+
AI agents must inspect the entity first, summarize the exact entity, target
|
|
126
|
+
state, and side effects, then wait for explicit user confirmation before running
|
|
127
|
+
the sensitive command.
|
|
128
|
+
|
|
122
129
|
Budgets
|
|
123
130
|
list-budgets [--projectId] [--name] [--status CSV] [--createdBy] [--dateFrom] [--dateTo] [--sortBy] [--sortDir] [--page] [--perPage] [--summary]
|
|
124
131
|
get-budget <id> Budget + line items
|
|
@@ -127,6 +134,7 @@ Budgets
|
|
|
127
134
|
get-budget-categories
|
|
128
135
|
get-budget-versions <budgetId>
|
|
129
136
|
update-budget-status <id> <status> [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
137
|
+
Budget-changing commands require interactive "yes" confirmation when the current budget is ESTIMATE_CREATED, ESTIMATE_SENT, ESTIMATE_ACCEPTED, ESTIMATE_REJECTED, or ESTIMATE_CLOSED.
|
|
130
138
|
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, and auto-creates the Asana project/section when enabled.
|
|
131
139
|
ESTIMATE_REJECTED requires --projectStatusOnCommercialRejection when rejecting the only accepted/closed budget on a commercial project.
|
|
132
140
|
status: ${budgetStatusesForHelp.join(", ")}
|
|
@@ -152,6 +160,7 @@ Bills
|
|
|
152
160
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
153
161
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
154
162
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
163
|
+
New supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for each non-legacy line item.
|
|
155
164
|
create-bill-approval <billId> (also sends approval request emails)
|
|
156
165
|
create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; use upload-bill-attachment after create to add files)
|
|
157
166
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
@@ -165,8 +174,18 @@ Bills
|
|
|
165
174
|
upload-bill-attachment <billId> <filePath> Presigned S3 upload + confirm (same flow as web UI)
|
|
166
175
|
get-bill-details <billId>
|
|
167
176
|
|
|
177
|
+
Quotations
|
|
178
|
+
list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
|
|
179
|
+
get-quotation <quotationId>
|
|
180
|
+
upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
|
|
181
|
+
create-quotation --payload '<json>' (quotation.createDraft)
|
|
182
|
+
submit-quotation <quotationId>
|
|
183
|
+
approve-quotation <quotationId>
|
|
184
|
+
reject-quotation <quotationId> --reason <text>
|
|
185
|
+
download-quotation-pdf <quotationId> [original|staff|final]
|
|
186
|
+
|
|
168
187
|
Approvals
|
|
169
|
-
list-approvals | get-pending-approvals [--type budget|supplier|bill|all]
|
|
188
|
+
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|all]
|
|
170
189
|
approve-bill <billId> (also sends requester reply email)
|
|
171
190
|
reject-bill <billId> --reason <text> (also sends requester reply email)
|
|
172
191
|
approve-budget <budgetId> (also sends requester reply email)
|
|
@@ -184,7 +203,7 @@ Companies & projects
|
|
|
184
203
|
list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
|
|
185
204
|
Omit --active to include both active and inactive projects.
|
|
186
205
|
get-project <id>
|
|
187
|
-
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--pax] [--endDate <ISO>] [--description]
|
|
206
|
+
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--pax] [--endDate <ISO>] [--description]
|
|
188
207
|
update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
|
|
189
208
|
delete-project <id>
|
|
190
209
|
update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
|
|
@@ -674,6 +693,88 @@ async function main() {
|
|
|
674
693
|
await getBillDetails(id);
|
|
675
694
|
break;
|
|
676
695
|
}
|
|
696
|
+
case "list-quotations": {
|
|
697
|
+
const statusRaw = getFlag(flags, "status");
|
|
698
|
+
await listQuotations({
|
|
699
|
+
projectId: getFlag(flags, "projectId"),
|
|
700
|
+
budgetId: getFlag(flags, "budgetId"),
|
|
701
|
+
supplierId: getFlag(flags, "supplierId"),
|
|
702
|
+
status: statusRaw !== undefined
|
|
703
|
+
? String(statusRaw)
|
|
704
|
+
.split(",")
|
|
705
|
+
.map((value) => value.trim().toUpperCase())
|
|
706
|
+
.filter(Boolean)
|
|
707
|
+
: undefined,
|
|
708
|
+
page: getFlag(flags, "page") !== undefined
|
|
709
|
+
? Number(getFlag(flags, "page"))
|
|
710
|
+
: undefined,
|
|
711
|
+
pageSize: getFlag(flags, "pageSize") !== undefined
|
|
712
|
+
? Number(getFlag(flags, "pageSize"))
|
|
713
|
+
: undefined,
|
|
714
|
+
});
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "get-quotation": {
|
|
718
|
+
const id = positional[0];
|
|
719
|
+
if (!id)
|
|
720
|
+
throw new Error("get-quotation requires <quotationId>");
|
|
721
|
+
await getQuotationDetails(id);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "upload-quotation-attachment": {
|
|
725
|
+
const [projectId, filePath] = positional;
|
|
726
|
+
if (!projectId || !filePath) {
|
|
727
|
+
throw new Error("upload-quotation-attachment requires <projectId> <filePath>");
|
|
728
|
+
}
|
|
729
|
+
await uploadQuotationAttachmentFromPath(projectId, filePath);
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case "create-quotation": {
|
|
733
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
734
|
+
if (!payloadRaw) {
|
|
735
|
+
throw new Error("create-quotation requires --payload '<json>'");
|
|
736
|
+
}
|
|
737
|
+
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
738
|
+
await createQuotationFromPayload(raw);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
case "submit-quotation": {
|
|
742
|
+
const id = positional[0];
|
|
743
|
+
if (!id)
|
|
744
|
+
throw new Error("submit-quotation requires <quotationId>");
|
|
745
|
+
await submitQuotation(id);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case "approve-quotation": {
|
|
749
|
+
const id = positional[0];
|
|
750
|
+
if (!id)
|
|
751
|
+
throw new Error("approve-quotation requires <quotationId>");
|
|
752
|
+
await approveQuotation(id);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case "reject-quotation": {
|
|
756
|
+
const id = positional[0];
|
|
757
|
+
const reason = getFlag(flags, "reason");
|
|
758
|
+
if (!id || reason === undefined) {
|
|
759
|
+
throw new Error("reject-quotation requires <quotationId> --reason <text>");
|
|
760
|
+
}
|
|
761
|
+
await rejectQuotation(id, reason);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "download-quotation-pdf": {
|
|
765
|
+
const id = positional[0];
|
|
766
|
+
if (!id) {
|
|
767
|
+
throw new Error("download-quotation-pdf requires <quotationId> [original|staff|final]");
|
|
768
|
+
}
|
|
769
|
+
const roleRaw = (positional[1] ?? "final").toLowerCase();
|
|
770
|
+
const fileRole = roleRaw === "original"
|
|
771
|
+
? "ORIGINAL"
|
|
772
|
+
: roleRaw === "staff"
|
|
773
|
+
? "STAFF_SIGNED"
|
|
774
|
+
: "FINAL_SIGNED";
|
|
775
|
+
await downloadQuotationPdf(id, fileRole);
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
677
778
|
case "list-approvals":
|
|
678
779
|
case "get-pending-approvals": {
|
|
679
780
|
const typeRaw = getFlag(flags, "type");
|
|
@@ -811,6 +912,9 @@ async function main() {
|
|
|
811
912
|
const slackChannelUrl = getFlag(flags, "slackChannelUrl");
|
|
812
913
|
const slackChannelName = getFlag(flags, "slackChannelName");
|
|
813
914
|
const startDate = getFlag(flags, "startDate");
|
|
915
|
+
if (getFlag(flags, "requestQboAccountantNotification") !== undefined) {
|
|
916
|
+
throw new Error("create-project does not allow --requestQboAccountantNotification; CLI project creation always requests the QuickBooks project import like the web app.");
|
|
917
|
+
}
|
|
814
918
|
if (!name ||
|
|
815
919
|
!companyId ||
|
|
816
920
|
!contactPersonId ||
|
|
@@ -839,7 +943,6 @@ async function main() {
|
|
|
839
943
|
startDate,
|
|
840
944
|
endDate: getFlag(flags, "endDate"),
|
|
841
945
|
description: getFlag(flags, "description"),
|
|
842
|
-
requestQboAccountantNotification: parseOptionalBoolFlag(flags, "requestQboAccountantNotification"),
|
|
843
946
|
});
|
|
844
947
|
break;
|
|
845
948
|
}
|
package/dist/parse-cli-enums.js
CHANGED
|
@@ -215,7 +215,7 @@ export function parseOptionalBillListSortDir(raw) {
|
|
|
215
215
|
return raw;
|
|
216
216
|
}
|
|
217
217
|
function isPendingRequestType(s) {
|
|
218
|
-
return s === "BUDGET" || s === "BILL" || s === "SUPPLIER";
|
|
218
|
+
return (s === "BUDGET" || s === "BILL" || s === "SUPPLIER" || s === "QUOTATION");
|
|
219
219
|
}
|
|
220
220
|
export function parseApprovalTypeFlag(raw) {
|
|
221
221
|
if (raw === undefined)
|
|
@@ -224,7 +224,7 @@ export function parseApprovalTypeFlag(raw) {
|
|
|
224
224
|
if (u === "ALL")
|
|
225
225
|
return "ALL";
|
|
226
226
|
if (!isPendingRequestType(u)) {
|
|
227
|
-
throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, or all.`);
|
|
227
|
+
throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, quotation, or all.`);
|
|
228
228
|
}
|
|
229
229
|
return u;
|
|
230
230
|
}
|
package/dist/prisma-enums.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Budget Builder CLI for AI agents — manage budgets, bills, and claims; bill records use isClaimable=false for bills and isClaimable=true for claims.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|