@go-labs-sg/bb 2.29.0 → 2.31.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 +17 -1
- package/command-manifest.json +5 -1
- package/command-reference.md +1 -1
- package/dist/index.js +71 -26
- package/package.json +1 -1
- package/role-aware-agent-guide.md +2 -0
package/README.md
CHANGED
|
@@ -230,6 +230,10 @@ commands return a conflict until delivery becomes `SENT` or `FAILED`.
|
|
|
230
230
|
|
|
231
231
|
Every canonical command is classified by effect: `state-change`, `email`, `external-write`, `delete`, and/or `financial-write`. Interactive runs require one exact `CONFIRM`. Non-interactive runs require the corresponding flags (`--allow-state-change`, `--allow-email`, `--allow-external-write`, `--allow-delete`, and `--allow-financial-write`); commands with multiple effects require every matching flag. Agents must still get user confirmation in chat first—the runtime gate is not user authorization.
|
|
232
232
|
|
|
233
|
+
**Draft-only bills:** Use `bb bill create --draft-only --payload '<bill-json>' --allow-state-change --allow-financial-write` when the user authorizes a draft only. The equivalent payload field is `draftOnly: true`. This creates `DRAFT` for every role, without auto-checking, auto-approval, approval requests, emails, or QuickBooks writes. It cannot be combined with `alreadyPaid: true`; conflicting flag and payload values are rejected. Draft-only creation uses the dedicated `bill.createDraft` API route and fails on older servers without falling back to ordinary creation. Deploy backend support before releasing the updated CLI. Reconciled projects must be moved to Won separately before draft-only creation, because reopening a project can trigger external integrations. Completed projects retain their existing creation restriction.
|
|
234
|
+
|
|
235
|
+
Bill creation determines its required permissions from the parsed input, for both canonical and legacy commands. Ordinary unpaid creation requires state-change, financial-write, and external-write permissions, but no email permission. Already-paid creation also requires email permission. Without draft-only mode, existing role behavior remains: ordinary users create drafts, Accounting creates checked bills, and Admin creates approved bills and writes to QuickBooks. Ordinary creation can also adjust existing QuickBooks placeholder bills. Explicit drafts defer those adjustments until the later approval/QuickBooks workflow. When later authorized to submit the draft, use `bb bill approval create <billId>` with its state-change and email permissions; Admin-created drafts also enter finance review through this command. The static command catalog lists the union of possible effects.
|
|
236
|
+
|
|
233
237
|
**Approval email exclusions:** Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
|
|
234
238
|
|
|
235
239
|
**Locked estimate budget changes:** Legacy flat commands retain their additional interactive `yes` prompt when the current budget is locked. Canonical v2 commands use the single effect-aware confirmation gate; non-interactive execution requires `--allow-state-change` plus any other effects declared for that command.
|
|
@@ -281,7 +285,7 @@ Bill, claim, and quotation mutations accept only suppliers whose approval status
|
|
|
281
285
|
| **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`, `rename-budget-version`, `restore-budget-version`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--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`; cloned unavailable lines require `unavailableItemReviewAcknowledged: true`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `create-estimate`, `send-estimate-to-contact-person` (same contact-person email workflow as web), `add-budget-items`, `update-budget-item`, `replace-budget-item`, `approve-unavailable-item-exception` (Lead/Admin), `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `mark-budget-items-not-utilized`, `restore-budget-item`, `create-placeholder-bill` (admin recovery), `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `delete-budget-commission`, `update-budget-discount`, `delete-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
282
286
|
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`; validates the project and supplier separately, then checks approved quotation links for each selected budget line item independent of the bill supplier; `alreadyPaid` never bypasses the checks), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON; ownership and one-hour expiry are enforced by a server-side staged-upload record rather than encoded in the object key), `cleanup-staged-bill-attachments`, `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`; admin creation automatically queues QBO finalization while other roles remain pending approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage linked to every selected line item, independent of the bill supplier, even when already paid), `update-bill` (`--payload`), `update-bill-payment-evidence` (`--payload`; replaces the payment reference and payment-proof PDFs for an already-paid bill), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (`PAID` requires `--paymentReference`; pass `--paymentProof <receipt.pdf>` to stage and submit a PDF up to 20MB atomically, or omit it only when BB already has payment proof; moving to `PAID` runs the server's paid-bill notification workflow), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
283
287
|
| **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
284
|
-
| **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `list-customer-invoices` (global/project/budget filters plus summary metrics), `get-customer-invoice`, `get-customer-invoice-email-context`, `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice` (admin), `reject-customer-invoice` (admin), `send-customer-invoice-to-contact-person`, `download-customer-invoice-pdf` (non-admins cannot download while approval is pending), `sync-customer-invoice` |
|
|
288
|
+
| **Customer invoices** | `check-customer-invoice-readiness`, `list-eligible-customer-invoice-budgets`, `list-customer-invoices` (global/project/budget filters plus summary metrics; `--sortBy totalInvoiceAmount` sorts provider-confirmed totals including GST, with unknown amounts last), `get-customer-invoice`, `get-customer-invoice-email-context`, `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice` (admin), `reject-customer-invoice` (admin), `send-customer-invoice-to-contact-person`, `download-customer-invoice-pdf` (non-admins cannot download while approval is pending), `sync-customer-invoice` |
|
|
285
289
|
| **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|customer_invoice\|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`, `approve-customer-invoice` / `reject-customer-invoice` |
|
|
286
290
|
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project` (its budget overview returns `totalRevenue`, calculated only from Estimate Accepted and Estimate Closed budgets), `get-project-hub-status`, `setup-project-hub`, `sync-project-hub-commercial-documents`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`, `--projectManagerId`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `projectManagerId` and `requestQboAccountantNotification` in JSON), `delete-project`, `check-project-reconciliation <id>` (runs the web app's live checks without changing status), `reconcile-project <id>` (reruns checks transactionally and marks an eligible project `RECONCILED`), `complete-project <id>` (reruns validation, marks a reconciled project `COMPLETED`, and queues QuickBooks placeholder cleanup), `import-qbo-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `RECONCILED` \| `LOST`; close-out follows `WON` → `RECONCILED` → `COMPLETED`, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; marking `COMPLETED` queues deletion of every remaining project placeholder bill from QuickBooks; for `PITCH` → `WON` also pass a Budget Builder user `--projectManagerId` or `--projectManagerEmail`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
|
|
287
291
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
@@ -345,3 +349,15 @@ The list also accepts `PARTIALLY_SENT`, `PARTIALLY_PAID`, `PAID`, and `VOIDED`,
|
|
|
345
349
|
alongside existing approval states such as `APPROVED` and `PENDING_APPROVAL`.
|
|
346
350
|
`--sortBy status` sorts by lifecycle; `lifecycleStatus` in list/detail output is
|
|
347
351
|
separate from the stored approval/operational `status`.
|
|
352
|
+
|
|
353
|
+
### Shared allowance decisions
|
|
354
|
+
|
|
355
|
+
Quotation approval/submission and bill approval/status commands accept an optional
|
|
356
|
+
`--overBudgetOverrideReason "<reason>"`. Bill create/update JSON payloads accept
|
|
357
|
+
`overBudgetOverrideReason`. Only admins may override an assessed overrun; missing
|
|
358
|
+
relationships or allocations block approval and require review. Multi-line quotation
|
|
359
|
+
create/update payloads must include `lineAmounts` with `{ budgetItemId, amount }`
|
|
360
|
+
for every selected line, summing to `amountWithoutGst`. The server revalidates
|
|
361
|
+
existing quotation links, reviewed usage, and per-line allowance inside the
|
|
362
|
+
approval transaction. No new database fields or relationship declarations are
|
|
363
|
+
required; unresolved historical overlaps continue to block approval.
|
package/command-manifest.json
CHANGED
|
@@ -2063,7 +2063,7 @@
|
|
|
2063
2063
|
{
|
|
2064
2064
|
"path": ["bill", "create"],
|
|
2065
2065
|
"legacyAliases": ["create-bill", "create_bill"],
|
|
2066
|
-
"summary": "Create a bill or claim with an approved supplier.",
|
|
2066
|
+
"summary": "Create a bill or claim with an approved supplier. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.",
|
|
2067
2067
|
"globalOptions": [
|
|
2068
2068
|
{
|
|
2069
2069
|
"name": "--help",
|
|
@@ -2104,6 +2104,10 @@
|
|
|
2104
2104
|
{
|
|
2105
2105
|
"name": "--allow-financial-write",
|
|
2106
2106
|
"description": "Allow financial-record changes in non-interactive use."
|
|
2107
|
+
},
|
|
2108
|
+
{
|
|
2109
|
+
"name": "--draft-only",
|
|
2110
|
+
"description": "Create a draft for every role; no approval, email, or QuickBooks writes. Conflicts with alreadyPaid: true."
|
|
2107
2111
|
}
|
|
2108
2112
|
],
|
|
2109
2113
|
"argumentMode": "legacy-passthrough",
|
package/command-reference.md
CHANGED
|
@@ -347,7 +347,7 @@ Effects: state-change, email.
|
|
|
347
347
|
|
|
348
348
|
## `bb bill create`
|
|
349
349
|
|
|
350
|
-
Create a bill or claim with an approved supplier.
|
|
350
|
+
Create a bill or claim with an approved supplier. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.
|
|
351
351
|
|
|
352
352
|
Legacy aliases: `create-bill`, `create_bill`.
|
|
353
353
|
|
package/dist/index.js
CHANGED
|
@@ -20259,7 +20259,7 @@ var billAttachment = (x, i, field = "attachments") => {
|
|
|
20259
20259
|
size: Number(a.size)
|
|
20260
20260
|
};
|
|
20261
20261
|
};
|
|
20262
|
-
function parseCreateBillPayload(raw) {
|
|
20262
|
+
function parseCreateBillPayload(raw, draftOnlyFlag) {
|
|
20263
20263
|
const o = requireObject(raw, "create-bill payload");
|
|
20264
20264
|
const isClaimable = o.isClaimable;
|
|
20265
20265
|
if (typeof isClaimable !== "boolean") {
|
|
@@ -20271,7 +20271,18 @@ function parseCreateBillPayload(raw) {
|
|
|
20271
20271
|
}
|
|
20272
20272
|
const attachmentsRaw = o.attachments;
|
|
20273
20273
|
const attachments = Array.isArray(attachmentsRaw) ? attachmentsRaw.map((x, i) => billAttachment(x, i)) : [];
|
|
20274
|
+
const draftOnlyRaw = o.draftOnly;
|
|
20275
|
+
if (draftOnlyRaw !== undefined && typeof draftOnlyRaw !== "boolean") {
|
|
20276
|
+
throw new Error("create-bill payload.draftOnly must be boolean.");
|
|
20277
|
+
}
|
|
20278
|
+
if (draftOnlyFlag !== undefined && draftOnlyRaw !== undefined && draftOnlyFlag !== draftOnlyRaw) {
|
|
20279
|
+
throw new Error("--draft-only conflicts with payload.draftOnly.");
|
|
20280
|
+
}
|
|
20281
|
+
const draftOnly = draftOnlyFlag ?? draftOnlyRaw;
|
|
20274
20282
|
const alreadyPaidRaw = o.alreadyPaid;
|
|
20283
|
+
if (draftOnly && alreadyPaidRaw === true) {
|
|
20284
|
+
throw new Error("Draft-only creation cannot be combined with alreadyPaid: true.");
|
|
20285
|
+
}
|
|
20275
20286
|
if (alreadyPaidRaw !== undefined && alreadyPaidRaw !== null && typeof alreadyPaidRaw !== "boolean") {
|
|
20276
20287
|
throw new Error("create-bill payload.alreadyPaid must be boolean.");
|
|
20277
20288
|
}
|
|
@@ -20315,6 +20326,7 @@ function parseCreateBillPayload(raw) {
|
|
|
20315
20326
|
isClaimable,
|
|
20316
20327
|
attachments,
|
|
20317
20328
|
alreadyPaid: alreadyPaidRaw ?? undefined,
|
|
20329
|
+
draftOnly,
|
|
20318
20330
|
paymentReference: optionalString(o.paymentReference),
|
|
20319
20331
|
paymentProofAttachments,
|
|
20320
20332
|
extractedSupplierName: optionalString(o.extractedSupplierName),
|
|
@@ -20327,8 +20339,12 @@ function parseCreateBillPayload(raw) {
|
|
|
20327
20339
|
}
|
|
20328
20340
|
function parseUpdateBillPayload(raw) {
|
|
20329
20341
|
const o = requireObject(raw, "update-bill payload");
|
|
20342
|
+
const { draftOnly, ...input2 } = parseCreateBillPayload(raw);
|
|
20343
|
+
if (draftOnly !== undefined) {
|
|
20344
|
+
throw new Error("draftOnly is only supported when creating a bill.");
|
|
20345
|
+
}
|
|
20330
20346
|
return {
|
|
20331
|
-
...
|
|
20347
|
+
...input2,
|
|
20332
20348
|
id: requiredString(o.id, "update-bill payload.id")
|
|
20333
20349
|
};
|
|
20334
20350
|
}
|
|
@@ -21310,7 +21326,7 @@ async function listBills(opts) {
|
|
|
21310
21326
|
const result = await api2.bill.getAll.query(input2);
|
|
21311
21327
|
out(result);
|
|
21312
21328
|
}
|
|
21313
|
-
async function approveBill(billId) {
|
|
21329
|
+
async function approveBill(billId, overBudgetOverrideReason) {
|
|
21314
21330
|
await assertSensitiveWorkflowConfirmed({
|
|
21315
21331
|
action: "Approve bill",
|
|
21316
21332
|
entity: `bill ${billId}`,
|
|
@@ -21324,7 +21340,8 @@ async function approveBill(billId) {
|
|
|
21324
21340
|
}
|
|
21325
21341
|
const result = await api2.bill.updateApproval.mutate({
|
|
21326
21342
|
billApprovalId: approval.id,
|
|
21327
|
-
status: "APPROVED"
|
|
21343
|
+
status: "APPROVED",
|
|
21344
|
+
overBudgetOverrideReason
|
|
21328
21345
|
});
|
|
21329
21346
|
if (result.processing) {
|
|
21330
21347
|
out(result);
|
|
@@ -21503,7 +21520,8 @@ async function updateBillStatus(opts) {
|
|
|
21503
21520
|
rejectionReason: opts.rejectionReason,
|
|
21504
21521
|
paymentTrackingUrl: opts.paymentTrackingUrl,
|
|
21505
21522
|
paymentReference: opts.paymentReference,
|
|
21506
|
-
paymentProofAttachments: paymentProof ? [paymentProof] : undefined
|
|
21523
|
+
paymentProofAttachments: paymentProof ? [paymentProof] : undefined,
|
|
21524
|
+
overBudgetOverrideReason: opts.overBudgetOverrideReason
|
|
21507
21525
|
});
|
|
21508
21526
|
out(result);
|
|
21509
21527
|
} catch (error61) {
|
|
@@ -22030,9 +22048,24 @@ async function deleteItemCategoriesByIds(ids) {
|
|
|
22030
22048
|
const result = await api2.itemCategory.deleteItemCategories.mutate({ ids });
|
|
22031
22049
|
out(result);
|
|
22032
22050
|
}
|
|
22033
|
-
async function createBillFromPayload(raw) {
|
|
22034
|
-
const input2 = parseCreateBillPayload(raw);
|
|
22035
|
-
|
|
22051
|
+
async function createBillFromPayload(raw, draftOnlyFlag) {
|
|
22052
|
+
const input2 = parseCreateBillPayload(raw, draftOnlyFlag);
|
|
22053
|
+
await confirmCurrentCommand({
|
|
22054
|
+
action: input2.draftOnly ? "bill create (draft only)" : "bill create",
|
|
22055
|
+
target: `project ${input2.projectId}, supplier ${input2.supplierId}`,
|
|
22056
|
+
effects: [
|
|
22057
|
+
"state-change",
|
|
22058
|
+
"financial-write",
|
|
22059
|
+
...input2.draftOnly ? [] : ["external-write"],
|
|
22060
|
+
...input2.alreadyPaid ? ["email"] : []
|
|
22061
|
+
],
|
|
22062
|
+
details: input2.draftOnly ? "Create a DRAFT bill without approval, email, or QuickBooks writes." : "Uses the caller's normal bill creation workflow; Accounting auto-checks and Admin auto-approves unpaid bills."
|
|
22063
|
+
});
|
|
22064
|
+
const result = input2.draftOnly ? await api2.bill.createDraft.mutate({
|
|
22065
|
+
...input2,
|
|
22066
|
+
draftOnly: true,
|
|
22067
|
+
alreadyPaid: false
|
|
22068
|
+
}) : await api2.bill.create.mutate(input2);
|
|
22036
22069
|
out(result);
|
|
22037
22070
|
}
|
|
22038
22071
|
async function listQuotations(opts) {
|
|
@@ -22057,12 +22090,15 @@ var deleteQuotationById = async (id) => {
|
|
|
22057
22090
|
const result = await api2.quotation.delete.mutate({ id });
|
|
22058
22091
|
out(result);
|
|
22059
22092
|
};
|
|
22060
|
-
async function submitQuotation(id) {
|
|
22093
|
+
async function submitQuotation(id, overBudgetOverrideReason) {
|
|
22061
22094
|
await assertSensitiveWorkflowConfirmed({
|
|
22062
22095
|
action: "Submit quotation for approval",
|
|
22063
22096
|
entity: `quotation ${id}`
|
|
22064
22097
|
});
|
|
22065
|
-
const result = await api2.quotation.submitForApproval.mutate({
|
|
22098
|
+
const result = await api2.quotation.submitForApproval.mutate({
|
|
22099
|
+
id,
|
|
22100
|
+
overBudgetOverrideReason
|
|
22101
|
+
});
|
|
22066
22102
|
out(result);
|
|
22067
22103
|
}
|
|
22068
22104
|
async function downloadQuotationPdf(id, fileRole) {
|
|
@@ -22492,7 +22528,7 @@ async function rejectBill(billId, reason) {
|
|
|
22492
22528
|
email: email3
|
|
22493
22529
|
});
|
|
22494
22530
|
}
|
|
22495
|
-
async function approveQuotation(quotationId) {
|
|
22531
|
+
async function approveQuotation(quotationId, overBudgetOverrideReason) {
|
|
22496
22532
|
await assertSensitiveWorkflowConfirmed({
|
|
22497
22533
|
action: "Approve quotation",
|
|
22498
22534
|
entity: `quotation ${quotationId}`
|
|
@@ -22504,7 +22540,8 @@ async function approveQuotation(quotationId) {
|
|
|
22504
22540
|
throw new Error("No pending quotation approval found.");
|
|
22505
22541
|
}
|
|
22506
22542
|
const result = await api2.quotation.approve.mutate({
|
|
22507
|
-
quotationApprovalId: approval.id
|
|
22543
|
+
quotationApprovalId: approval.id,
|
|
22544
|
+
overBudgetOverrideReason
|
|
22508
22545
|
});
|
|
22509
22546
|
out(result);
|
|
22510
22547
|
}
|
|
@@ -23189,7 +23226,7 @@ var summaryFor = (legacyTarget) => {
|
|
|
23189
23226
|
if (legacyTarget === "whoami")
|
|
23190
23227
|
return "Show the active API-key identity.";
|
|
23191
23228
|
if (legacyTarget === "create-bill") {
|
|
23192
|
-
return "Create a bill or claim with an approved supplier.";
|
|
23229
|
+
return "Create a bill or claim with an approved supplier. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.";
|
|
23193
23230
|
}
|
|
23194
23231
|
if (legacyTarget === "validate-bill-selection") {
|
|
23195
23232
|
return "Validate the approved supplier and selected bill line items.";
|
|
@@ -23297,7 +23334,12 @@ var registry2 = [
|
|
|
23297
23334
|
legacyCommand("list-bills", ["bill", "list"]),
|
|
23298
23335
|
legacyCommand("list-claims", ["claim", "list"]),
|
|
23299
23336
|
legacyCommand("create-bill-approval", ["bill", "approval", "create"]),
|
|
23300
|
-
legacyCommand("create-bill", ["bill", "create"]
|
|
23337
|
+
legacyCommand("create-bill", ["bill", "create"], [], [
|
|
23338
|
+
{
|
|
23339
|
+
name: "--draft-only",
|
|
23340
|
+
description: "Create a draft for every role; no approval, email, or QuickBooks writes. Conflicts with alreadyPaid: true."
|
|
23341
|
+
}
|
|
23342
|
+
]),
|
|
23301
23343
|
legacyCommand("validate-bill-selection", ["bill", "selection", "validate"]),
|
|
23302
23344
|
legacyCommand("update-bill", ["bill", "update"]),
|
|
23303
23345
|
legacyCommand("update-bill-payment-evidence", [
|
|
@@ -23675,6 +23717,7 @@ function parseOptionalBoolFlag(flags, key) {
|
|
|
23675
23717
|
var CUSTOMER_INVOICE_SORT_FIELDS = [
|
|
23676
23718
|
"createdAt",
|
|
23677
23719
|
"status",
|
|
23720
|
+
"totalInvoiceAmount",
|
|
23678
23721
|
"totalNetAmount",
|
|
23679
23722
|
"approvalExpiresAt"
|
|
23680
23723
|
];
|
|
@@ -23843,14 +23886,14 @@ Bills
|
|
|
23843
23886
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
23844
23887
|
Non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage linked to every selected line item, independent of the bill supplier, including already-paid bills.
|
|
23845
23888
|
create-bill-approval <billId> (also queues approval request emails)
|
|
23846
|
-
create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; supplier must be APPROVED)
|
|
23889
|
+
create-bill --payload '<json>' [--draft-only] (bill.create; payload.isClaimable false = bill, true = claim; supplier must be APPROVED)
|
|
23847
23890
|
validate-bill-selection --payload '<json>' Require an APPROVED supplier, then verify approved quotation links for each budgetItemId; alreadyPaid never bypasses quotation checks.
|
|
23848
23891
|
stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
|
|
23849
23892
|
cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
|
|
23850
23893
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
23851
23894
|
update-bill-payment-evidence --payload '<json>' Replace paymentReference/paymentProofAttachments for an already-paid bill.
|
|
23852
23895
|
delete-bill <billId>
|
|
23853
|
-
update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference] [--paymentProof <receipt.pdf>]
|
|
23896
|
+
update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference] [--paymentProof <receipt.pdf>] [--overBudgetOverrideReason <text>]
|
|
23854
23897
|
status (${billStatusesForUpdateHelp.join(", ")}) \u2014 not PENDING_APPROVAL; use create-bill-approval
|
|
23855
23898
|
PAID requires --paymentReference and a payment-proof PDF. Use --paymentProof to stage and submit the PDF atomically, or omit it only when BB already has a BILL_PAYMENT_PROOF attachment.
|
|
23856
23899
|
Moving a bill or claim to PAID runs the server's paid-bill notification workflow.
|
|
@@ -23872,15 +23915,15 @@ Quotations
|
|
|
23872
23915
|
create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and gstAmount)
|
|
23873
23916
|
update-quotation --payload '<json>' Update a DRAFT or REJECTED quotation; same payload plus id.
|
|
23874
23917
|
delete-quotation <quotationId>
|
|
23875
|
-
submit-quotation <quotationId>
|
|
23876
|
-
approve-quotation <quotationId>
|
|
23918
|
+
submit-quotation <quotationId> [--overBudgetOverrideReason <text>]
|
|
23919
|
+
approve-quotation <quotationId> [--overBudgetOverrideReason <text>]
|
|
23877
23920
|
reject-quotation <quotationId> --reason <text>
|
|
23878
23921
|
download-quotation-pdf <quotationId> [original|staff|final]
|
|
23879
23922
|
|
|
23880
23923
|
Customer invoices
|
|
23881
23924
|
check-customer-invoice-readiness <budgetId>
|
|
23882
23925
|
list-eligible-customer-invoice-budgets <projectId> Accepted budgets that are ready and still have invoice coverage remaining.
|
|
23883
|
-
list-customer-invoices [budgetId] [--projectId <id>] [--companyIds CSV] [--projectIds CSV] [--budgetIds CSV] [--createdByIds CSV] [--status CSV] [--search] [--sortBy createdAt|status|
|
|
23926
|
+
list-customer-invoices [budgetId] [--projectId <id>] [--companyIds CSV] [--projectIds CSV] [--budgetIds CSV] [--createdByIds CSV] [--status CSV] [--search] [--sortBy createdAt|status|totalInvoiceAmount|approvalExpiresAt] [--sortDir asc|desc] [--page] [--perPage]
|
|
23884
23927
|
No scope flag lists invoices globally. --projectId uses the project scope. A positional budgetId is a backward-compatible shortcut for --budgetIds.
|
|
23885
23928
|
get-customer-invoice <batchId>
|
|
23886
23929
|
get-customer-invoice-email-context <batchId> Contact, required CC, budget, and project context for composing an invoice email.
|
|
@@ -23896,7 +23939,7 @@ Customer invoices
|
|
|
23896
23939
|
|
|
23897
23940
|
Approvals
|
|
23898
23941
|
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|customer_invoice|all]
|
|
23899
|
-
approve-bill <billId>
|
|
23942
|
+
approve-bill <billId> [--overBudgetOverrideReason <text>] (also sends requester reply email)
|
|
23900
23943
|
reject-bill <billId> --reason <text> (also sends requester reply email)
|
|
23901
23944
|
approve-budget <budgetId> (also sends requester reply email)
|
|
23902
23945
|
reject-budget <budgetId> --reason <text> (also sends requester reply email)
|
|
@@ -24328,7 +24371,7 @@ Waiting for approval\u2026
|
|
|
24328
24371
|
}
|
|
24329
24372
|
configureAccessToken(accessToken);
|
|
24330
24373
|
}
|
|
24331
|
-
if (!resolvedCommand.isLegacyAlias && resolvedCommand.command.effects.length > 0) {
|
|
24374
|
+
if (!resolvedCommand.isLegacyAlias && cmd !== "create-bill" && resolvedCommand.command.effects.length > 0) {
|
|
24332
24375
|
await confirmCurrentCommand({
|
|
24333
24376
|
action: resolvedCommand.command.path.join(" "),
|
|
24334
24377
|
target: commandConfirmationTarget(resolvedCommand),
|
|
@@ -24695,7 +24738,8 @@ Waiting for approval\u2026
|
|
|
24695
24738
|
throw new Error("create-bill requires --payload '<json>'");
|
|
24696
24739
|
}
|
|
24697
24740
|
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
24698
|
-
|
|
24741
|
+
const draftOnly = flags["draft-only"] === true ? true : parseOptionalBoolFlag(flags, "draft-only");
|
|
24742
|
+
await createBillFromPayload(raw, draftOnly);
|
|
24699
24743
|
break;
|
|
24700
24744
|
}
|
|
24701
24745
|
case "validate-bill-selection": {
|
|
@@ -24748,7 +24792,8 @@ Waiting for approval\u2026
|
|
|
24748
24792
|
rejectionReason: getFlag(flags, "rejectionReason"),
|
|
24749
24793
|
paymentTrackingUrl: getFlag(flags, "paymentTrackingUrl"),
|
|
24750
24794
|
paymentReference: getFlag(flags, "paymentReference"),
|
|
24751
|
-
paymentProofPath: paymentProofFlag
|
|
24795
|
+
paymentProofPath: paymentProofFlag,
|
|
24796
|
+
overBudgetOverrideReason: getFlag(flags, "overBudgetOverrideReason")
|
|
24752
24797
|
});
|
|
24753
24798
|
break;
|
|
24754
24799
|
}
|
|
@@ -24986,14 +25031,14 @@ Waiting for approval\u2026
|
|
|
24986
25031
|
const id = positional[0];
|
|
24987
25032
|
if (!id)
|
|
24988
25033
|
throw new Error("submit-quotation requires <quotationId>");
|
|
24989
|
-
await submitQuotation(id);
|
|
25034
|
+
await submitQuotation(id, getFlag(flags, "overBudgetOverrideReason"));
|
|
24990
25035
|
break;
|
|
24991
25036
|
}
|
|
24992
25037
|
case "approve-quotation": {
|
|
24993
25038
|
const id = positional[0];
|
|
24994
25039
|
if (!id)
|
|
24995
25040
|
throw new Error("approve-quotation requires <quotationId>");
|
|
24996
|
-
await approveQuotation(id);
|
|
25041
|
+
await approveQuotation(id, getFlag(flags, "overBudgetOverrideReason"));
|
|
24997
25042
|
break;
|
|
24998
25043
|
}
|
|
24999
25044
|
case "reject-quotation": {
|
|
@@ -25163,7 +25208,7 @@ Waiting for approval\u2026
|
|
|
25163
25208
|
const id = positional[0];
|
|
25164
25209
|
if (!id)
|
|
25165
25210
|
throw new Error("approve-bill requires <billId>");
|
|
25166
|
-
await approveBill(id);
|
|
25211
|
+
await approveBill(id, getFlag(flags, "overBudgetOverrideReason"));
|
|
25167
25212
|
break;
|
|
25168
25213
|
}
|
|
25169
25214
|
case "reject-bill": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.0",
|
|
4
4
|
"description": "Budget Builder CLI for AI agents — manage budgets, bills, claims, quotations, and customer invoices with explicit workflow previews for sensitive changes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -129,6 +129,8 @@ The following distinctions are especially important:
|
|
|
129
129
|
- Project Hub setup and sync are asynchronous external writes. Read `bb project hub status <project-id>` first, obtain explicit approval, and request the mutation once instead of repeatedly queueing it. Setup readiness remains visible through `status`; commercial sync returns an outbox operation ID for Admin diagnostics.
|
|
130
130
|
- Bill/claim status commands have separate role rules for `CHECKED`, `APPROVED`, and `PAID`.
|
|
131
131
|
- `bb bill create --payload` uses `isClaimable=false`; `isClaimable=true` creates a claim. Claims intentionally permit some base-user flows that supplier bills do not.
|
|
132
|
+
- For a draft-only bill or claim, use `bb bill create --draft-only --payload` (or `draftOnly: true` in the payload). This forces `DRAFT` for every role and requires only `--allow-state-change --allow-financial-write`. It sends no approval/email and performs no QuickBooks writes. Do not combine it with `alreadyPaid: true`. An older API without `bill.createDraft` rejects the command; never retry with ordinary creation to bypass that error.
|
|
133
|
+
- Ordinary unpaid creation does not require `--allow-email`, but remains role-dependent: Accounting auto-checks and Admin auto-approves. It still requires `--allow-external-write`, including for possible placeholder adjustments. Already-paid creation retains the email gate.
|
|
132
134
|
- Staged attachments belong to the user in the database. Do not reuse, share, or manually construct staged keys.
|
|
133
135
|
|
|
134
136
|
## Handling authorization failures
|