@go-labs-sg/bb 1.18.1 → 1.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/README.md +11 -7
- package/dist/commands.js +261 -5
- package/dist/index.js +338 -15
- package/dist/parse-cli-enums.js +41 -3
- package/dist/parse-mutation-payload.js +36 -0
- package/dist/prisma-enums.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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.
|
|
9
|
+
For AI agents using this CLI: do not execute approval decisions, approval requests, status updates, customer-invoice changes, integration retries, `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
10
|
|
|
11
11
|
**Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
|
|
12
12
|
|
|
@@ -72,7 +72,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
72
72
|
|
|
73
73
|
**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 an Asana Event Ops section from Prompt 5 tasks. 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. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
|
|
74
74
|
|
|
75
|
-
**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.
|
|
75
|
+
**Sensitive workflow changes:** Approval decisions/requests, entity status updates, customer-invoice changes, integration retries, `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.
|
|
76
76
|
|
|
77
77
|
**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.
|
|
78
78
|
|
|
@@ -84,17 +84,21 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
84
84
|
|
|
85
85
|
| Area | Commands (non-exhaustive) |
|
|
86
86
|
| --- | --- |
|
|
87
|
-
| **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 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`), `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`) |
|
|
88
|
-
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `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`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (moving a bill or claim to `PAID` sends creator and supplier emails, CC `m@getout.sg` and BCC `noah@getout.sg`; a claim sends one email when both recipient addresses match), `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` |
|
|
89
|
-
| **Quotations** | `list-quotations
|
|
90
|
-
| **
|
|
91
|
-
| **
|
|
87
|
+
| **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`), `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`, `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`) |
|
|
88
|
+
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`; runs the same supplier, line-item, and quotation-coverage checks as the web flow), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `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`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `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` (moving a bill or claim to `PAID` sends creator and supplier emails, CC `m@getout.sg` and BCC `noah@getout.sg`; a claim sends one email when both recipient addresses match), `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` |
|
|
89
|
+
| **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`) |
|
|
90
|
+
| **Customer invoices** | `check-customer-invoice-readiness`, `list-customer-invoices`, `get-customer-invoice`, `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice`, `reject-customer-invoice`, `download-customer-invoice-pdf`, `sync-customer-invoice` |
|
|
91
|
+
| **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` |
|
|
92
|
+
| **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), `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`) |
|
|
92
93
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
93
94
|
| **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) |
|
|
94
95
|
| **Dashboard & users** | `list-users`, `create-user` (admin; provisions an API-only service identity with no Google sign-in; `--email`, optional `--name`, optional `--role` defaulting to `USER`), `create-api-key` (admin; `--userId`, `--name`; raw key shown once), `list-api-keys` (admin; optional `--userId`), `revoke-api-key` (admin; key ID plus `--userId` for another user's key), `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
|
95
96
|
| **Errors** | `get-recent-errors`, `get-error-metrics` |
|
|
97
|
+
| **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` |
|
|
96
98
|
| **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
|
|
97
99
|
|
|
100
|
+
The CLI intentionally wraps low-level upload-request/confirm procedures into file-based commands and omits browser-only helpers such as navigation counts, combobox/facet data, recent-page bookkeeping, and live UI subscriptions. Operational workflows for bills, claims, quotations, customer invoices, project reconciliation/completion, and integration recovery are available directly.
|
|
101
|
+
|
|
98
102
|
## Output
|
|
99
103
|
|
|
100
104
|
- **Stdout:** JSON (pretty-printed)
|
package/dist/commands.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { contentType } from "mime-types";
|
|
6
6
|
import { api } from "./api-client.js";
|
|
7
7
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
|
|
8
8
|
import { billStatusesForApi, } from "./parse-cli-enums.js";
|
|
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";
|
|
9
|
+
import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseCreateCustomerInvoicePayload, parseCreateQuotationPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBillPaymentEvidencePayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, parseUpdateQuotationPayload, parseValidateBillSelectionPayload, } from "./parse-mutation-payload.js";
|
|
10
|
+
import { BudgetStatus, ProjectStatus, } from "./prisma-enums.js";
|
|
11
11
|
import { createRichTextFromPlainText } from "./rich-text.js";
|
|
12
12
|
const BUDGET = "BUDGET";
|
|
13
13
|
const BILL = "BILL";
|
|
14
14
|
const SUPPLIER = "SUPPLIER";
|
|
15
15
|
const QUOTATION = "QUOTATION";
|
|
16
|
+
const CUSTOMER_INVOICE = "CUSTOMER_INVOICE";
|
|
16
17
|
const ASANA_WON_LOST_SECTION_GIDS = new Set([
|
|
17
18
|
"1211678338364908",
|
|
18
19
|
"1211678338364907",
|
|
@@ -261,6 +262,16 @@ const billAttachmentContentTypeForFileName = (fileName) => {
|
|
|
261
262
|
}
|
|
262
263
|
return resolvedContentType;
|
|
263
264
|
};
|
|
265
|
+
const QUOTATION_ATTACHMENT_MAX_SIZE = 20 * 1024 * 1024;
|
|
266
|
+
const quotationAttachmentContentTypeForFileName = (fileName) => {
|
|
267
|
+
const resolvedContentType = contentTypeHeaderForFileName(fileName);
|
|
268
|
+
if (resolvedContentType === "application/pdf" ||
|
|
269
|
+
resolvedContentType === "image/jpeg" ||
|
|
270
|
+
resolvedContentType === "image/png") {
|
|
271
|
+
return resolvedContentType;
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`Unsupported quotation attachment type: ${fileName}. Use PDF, JPEG, or PNG.`);
|
|
274
|
+
};
|
|
264
275
|
const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
|
|
265
276
|
BudgetStatus.ESTIMATE_CREATED,
|
|
266
277
|
BudgetStatus.ESTIMATE_SENT,
|
|
@@ -533,6 +544,10 @@ export async function listApprovals(opts) {
|
|
|
533
544
|
id = n.supplierQuotation.id;
|
|
534
545
|
name = `Quotation ${n.supplierQuotation.supplier.name}`;
|
|
535
546
|
}
|
|
547
|
+
else if (n.type === CUSTOMER_INVOICE && n.customerInvoiceBatch) {
|
|
548
|
+
id = n.customerInvoiceBatch.id;
|
|
549
|
+
name = `Customer invoice ${n.customerInvoiceBatch.project.name}`;
|
|
550
|
+
}
|
|
536
551
|
else {
|
|
537
552
|
id = n.id;
|
|
538
553
|
name = "Unknown";
|
|
@@ -668,6 +683,13 @@ export async function stageBillAttachmentsFromPaths(projectId, filePaths) {
|
|
|
668
683
|
attachments,
|
|
669
684
|
});
|
|
670
685
|
}
|
|
686
|
+
export const cleanupStagedBillAttachments = async (projectId, keys) => {
|
|
687
|
+
const result = await api.attachment.deleteStagedBillAttachments.mutate({
|
|
688
|
+
projectId,
|
|
689
|
+
keys,
|
|
690
|
+
});
|
|
691
|
+
out(result);
|
|
692
|
+
};
|
|
671
693
|
export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
|
|
672
694
|
const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
|
|
673
695
|
out(confirmed);
|
|
@@ -740,6 +762,10 @@ export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [],
|
|
|
740
762
|
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
741
763
|
const buf = await readFile(filePath);
|
|
742
764
|
const fileName = basename(filePath);
|
|
765
|
+
if (buf.byteLength > QUOTATION_ATTACHMENT_MAX_SIZE) {
|
|
766
|
+
throw new Error("Quotation attachment must be 20MB or smaller.");
|
|
767
|
+
}
|
|
768
|
+
const attachmentContentType = quotationAttachmentContentTypeForFileName(fileName);
|
|
743
769
|
const ext = fileName
|
|
744
770
|
.split(".")
|
|
745
771
|
.pop()
|
|
@@ -752,7 +778,7 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
|
752
778
|
const res = await fetch(uploadUrl, {
|
|
753
779
|
method: "PUT",
|
|
754
780
|
body: buf,
|
|
755
|
-
headers: { "Content-Type":
|
|
781
|
+
headers: { "Content-Type": attachmentContentType },
|
|
756
782
|
});
|
|
757
783
|
if (!res.ok) {
|
|
758
784
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
@@ -764,6 +790,13 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
|
764
790
|
size: buf.byteLength,
|
|
765
791
|
});
|
|
766
792
|
}
|
|
793
|
+
export const cleanupStagedQuotationAttachments = async (projectId, keys) => {
|
|
794
|
+
const result = await api.quotation.deleteStagedAttachments.mutate({
|
|
795
|
+
projectId,
|
|
796
|
+
keys,
|
|
797
|
+
});
|
|
798
|
+
out(result);
|
|
799
|
+
};
|
|
767
800
|
export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
768
801
|
await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
|
|
769
802
|
const buf = await readFile(filePath);
|
|
@@ -990,6 +1023,68 @@ export async function updateBudgetDiscountFromPayload(raw) {
|
|
|
990
1023
|
const result = await api.budget.updateBudgetDiscount.mutate(input);
|
|
991
1024
|
out(result);
|
|
992
1025
|
}
|
|
1026
|
+
export const deleteBudgetDiscount = async (budgetId) => {
|
|
1027
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1028
|
+
action: "Remove budget discount",
|
|
1029
|
+
entity: `budget ${budgetId}`,
|
|
1030
|
+
details: "and return the budget to DRAFT",
|
|
1031
|
+
});
|
|
1032
|
+
const result = await api.budget.deleteBudgetDiscount.mutate({ budgetId });
|
|
1033
|
+
out(result);
|
|
1034
|
+
};
|
|
1035
|
+
export const deleteBudgetCommission = async (budgetId) => {
|
|
1036
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1037
|
+
action: "Remove budget commission",
|
|
1038
|
+
entity: `budget ${budgetId}`,
|
|
1039
|
+
details: "and return the budget to DRAFT",
|
|
1040
|
+
});
|
|
1041
|
+
const result = await api.budget.deleteBudgetCommission.mutate({ budgetId });
|
|
1042
|
+
out(result);
|
|
1043
|
+
};
|
|
1044
|
+
export const setBudgetItemsNotUtilized = async (budgetItemIds, notUtilized) => {
|
|
1045
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1046
|
+
action: notUtilized
|
|
1047
|
+
? "Mark budget lines Not Utilized"
|
|
1048
|
+
: "Restore budget line",
|
|
1049
|
+
entity: `budget item${budgetItemIds.length === 1 ? "" : "s"} ${budgetItemIds.join(", ")}`,
|
|
1050
|
+
details: notUtilized
|
|
1051
|
+
? "and remove linked QuickBooks placeholder expenses"
|
|
1052
|
+
: "and restore its QuickBooks placeholder expense when required",
|
|
1053
|
+
});
|
|
1054
|
+
const result = await api.budget.setBudgetItemsNotUtilized.mutate({
|
|
1055
|
+
budgetItemIds,
|
|
1056
|
+
notUtilized,
|
|
1057
|
+
});
|
|
1058
|
+
out(result);
|
|
1059
|
+
};
|
|
1060
|
+
export const createPlaceholderBillForBudgetItem = async (budgetItemId) => {
|
|
1061
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1062
|
+
action: "Create QuickBooks placeholder bill",
|
|
1063
|
+
entity: `budget item ${budgetItemId}`,
|
|
1064
|
+
});
|
|
1065
|
+
const result = await api.budget.createPlaceholderBillForBudgetItem.mutate({
|
|
1066
|
+
budgetItemId,
|
|
1067
|
+
});
|
|
1068
|
+
out(result);
|
|
1069
|
+
};
|
|
1070
|
+
export const renameBudgetVersion = async (versionId, newName) => {
|
|
1071
|
+
const result = await api.budgetVersion.renameBudgetVersion.mutate({
|
|
1072
|
+
versionId,
|
|
1073
|
+
newName,
|
|
1074
|
+
});
|
|
1075
|
+
out(result);
|
|
1076
|
+
};
|
|
1077
|
+
export const restoreBudgetVersion = async (versionId) => {
|
|
1078
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1079
|
+
action: "Restore budget version",
|
|
1080
|
+
entity: `budget version ${versionId}`,
|
|
1081
|
+
details: "replace the current budget, project fields, items, and attachments, clear its QuickBooks estimate link, and return it to DRAFT",
|
|
1082
|
+
});
|
|
1083
|
+
const result = await api.budgetVersion.restoreBudgetVersion.mutate({
|
|
1084
|
+
versionId,
|
|
1085
|
+
});
|
|
1086
|
+
out(result);
|
|
1087
|
+
};
|
|
993
1088
|
export async function deleteItemById(itemId) {
|
|
994
1089
|
const result = await api.item.deleteItem.mutate({ id: itemId });
|
|
995
1090
|
out(result);
|
|
@@ -1044,9 +1139,19 @@ export async function getQuotationDetails(id) {
|
|
|
1044
1139
|
out(result);
|
|
1045
1140
|
}
|
|
1046
1141
|
export async function createQuotationFromPayload(raw) {
|
|
1047
|
-
const
|
|
1142
|
+
const input = parseCreateQuotationPayload(raw);
|
|
1143
|
+
const result = await api.quotation.createDraft.mutate(input);
|
|
1048
1144
|
out(result);
|
|
1049
1145
|
}
|
|
1146
|
+
export const updateQuotationFromPayload = async (raw) => {
|
|
1147
|
+
const input = parseUpdateQuotationPayload(raw);
|
|
1148
|
+
const result = await api.quotation.updateDraftOrRejected.mutate(input);
|
|
1149
|
+
out(result);
|
|
1150
|
+
};
|
|
1151
|
+
export const deleteQuotationById = async (id) => {
|
|
1152
|
+
const result = await api.quotation.delete.mutate({ id });
|
|
1153
|
+
out(result);
|
|
1154
|
+
};
|
|
1050
1155
|
export async function submitQuotation(id) {
|
|
1051
1156
|
await assertSensitiveWorkflowConfirmed({
|
|
1052
1157
|
action: "Submit quotation for approval",
|
|
@@ -1062,11 +1167,121 @@ export async function downloadQuotationPdf(id, fileRole) {
|
|
|
1062
1167
|
});
|
|
1063
1168
|
out(result);
|
|
1064
1169
|
}
|
|
1170
|
+
export const checkCustomerInvoiceReadiness = async (budgetId) => {
|
|
1171
|
+
const result = await api.customerInvoice.getInvoiceReadiness.query({
|
|
1172
|
+
budgetId,
|
|
1173
|
+
});
|
|
1174
|
+
out(result);
|
|
1175
|
+
};
|
|
1176
|
+
export const listCustomerInvoices = async (input) => {
|
|
1177
|
+
const result = await api.customerInvoice.listByBudget.query(input);
|
|
1178
|
+
out(result);
|
|
1179
|
+
};
|
|
1180
|
+
export const getCustomerInvoice = async (batchId) => {
|
|
1181
|
+
const result = await api.customerInvoice.getInvoiceDetail.query({ batchId });
|
|
1182
|
+
out(result);
|
|
1183
|
+
};
|
|
1184
|
+
export const createCustomerInvoice = async (raw) => {
|
|
1185
|
+
const input = parseCreateCustomerInvoicePayload(raw);
|
|
1186
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1187
|
+
action: "Create customer invoice",
|
|
1188
|
+
entity: `budget ${input.budgetId}`,
|
|
1189
|
+
details: "create the invoice in QuickBooks and either approve it as admin or send approval-request emails",
|
|
1190
|
+
});
|
|
1191
|
+
const result = await api.customerInvoice.createInvoiceBatch.mutate(input);
|
|
1192
|
+
out(result);
|
|
1193
|
+
};
|
|
1194
|
+
export const discardCreatingCustomerInvoice = async (batchId) => {
|
|
1195
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1196
|
+
action: "Discard unfinished customer invoice",
|
|
1197
|
+
entity: `invoice batch ${batchId}`,
|
|
1198
|
+
});
|
|
1199
|
+
const result = await api.customerInvoice.discardCreatingInvoiceBatch.mutate({
|
|
1200
|
+
batchId,
|
|
1201
|
+
});
|
|
1202
|
+
out(result);
|
|
1203
|
+
};
|
|
1204
|
+
export const deleteCustomerInvoice = async (batchId) => {
|
|
1205
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1206
|
+
action: "Delete customer invoice",
|
|
1207
|
+
entity: `invoice batch ${batchId}`,
|
|
1208
|
+
details: "delete its invoices from QuickBooks and remove the local batch",
|
|
1209
|
+
});
|
|
1210
|
+
const result = await api.customerInvoice.deleteInvoiceBatch.mutate({
|
|
1211
|
+
batchId,
|
|
1212
|
+
});
|
|
1213
|
+
out(result);
|
|
1214
|
+
};
|
|
1215
|
+
export const voidCustomerInvoice = async (batchId) => {
|
|
1216
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1217
|
+
action: "Void customer invoice",
|
|
1218
|
+
entity: `invoice batch ${batchId}`,
|
|
1219
|
+
details: "void its invoices in QuickBooks",
|
|
1220
|
+
});
|
|
1221
|
+
const result = await api.customerInvoice.voidInvoiceBatch.mutate({ batchId });
|
|
1222
|
+
out(result);
|
|
1223
|
+
};
|
|
1224
|
+
export const approveCustomerInvoice = async (batchId) => {
|
|
1225
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1226
|
+
action: "Approve customer invoice",
|
|
1227
|
+
entity: `invoice batch ${batchId}`,
|
|
1228
|
+
details: "and notify its creator",
|
|
1229
|
+
});
|
|
1230
|
+
const result = await api.customerInvoice.approveInvoiceBatch.mutate({
|
|
1231
|
+
batchId,
|
|
1232
|
+
});
|
|
1233
|
+
out(result);
|
|
1234
|
+
};
|
|
1235
|
+
export const rejectCustomerInvoice = async (batchId, rejectionReason) => {
|
|
1236
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1237
|
+
action: "Reject customer invoice",
|
|
1238
|
+
entity: `invoice batch ${batchId}`,
|
|
1239
|
+
details: `with reason "${rejectionReason}", void it in QuickBooks, and notify its creator`,
|
|
1240
|
+
});
|
|
1241
|
+
const result = await api.customerInvoice.rejectInvoiceBatch.mutate({
|
|
1242
|
+
batchId,
|
|
1243
|
+
rejectionReason,
|
|
1244
|
+
});
|
|
1245
|
+
out(result);
|
|
1246
|
+
};
|
|
1247
|
+
export const downloadCustomerInvoicePdf = async (invoiceId, outputPath) => {
|
|
1248
|
+
const result = await api.customerInvoice.downloadPdf.mutate({ invoiceId });
|
|
1249
|
+
const resolvedOutputPath = outputPath?.trim() || result.fileName;
|
|
1250
|
+
const contents = Buffer.from(result.base64, "base64");
|
|
1251
|
+
await writeFile(resolvedOutputPath, contents);
|
|
1252
|
+
out({
|
|
1253
|
+
contentType: result.contentType,
|
|
1254
|
+
fileName: result.fileName,
|
|
1255
|
+
outputPath: resolvedOutputPath,
|
|
1256
|
+
size: contents.byteLength,
|
|
1257
|
+
});
|
|
1258
|
+
};
|
|
1259
|
+
export const syncCustomerInvoice = async (invoiceId) => {
|
|
1260
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1261
|
+
action: "Sync customer invoice from QuickBooks",
|
|
1262
|
+
entity: `invoice ${invoiceId}`,
|
|
1263
|
+
details: "and update its local status, QuickBooks metadata, balance, and history",
|
|
1264
|
+
});
|
|
1265
|
+
const result = await api.customerInvoice.syncInvoiceStatus.mutate({
|
|
1266
|
+
invoiceId,
|
|
1267
|
+
});
|
|
1268
|
+
out(result);
|
|
1269
|
+
};
|
|
1065
1270
|
export async function updateBillFromPayload(raw) {
|
|
1066
1271
|
const input = parseUpdateBillPayload(raw);
|
|
1067
1272
|
const result = await api.bill.update.mutate(input);
|
|
1068
1273
|
out(result);
|
|
1069
1274
|
}
|
|
1275
|
+
export const validateBillSelectionFromPayload = async (raw) => {
|
|
1276
|
+
const input = parseValidateBillSelectionPayload(raw);
|
|
1277
|
+
const result = await api.bill.validateSelection.mutate(input);
|
|
1278
|
+
out(result);
|
|
1279
|
+
};
|
|
1280
|
+
export const updateBillPaymentEvidenceFromPayload = async (raw) => {
|
|
1281
|
+
const input = parseUpdateBillPaymentEvidencePayload(raw);
|
|
1282
|
+
const result = await api.bill.updatePaymentEvidence.mutate(input);
|
|
1283
|
+
out(result);
|
|
1284
|
+
};
|
|
1070
1285
|
export async function deleteBillById(id) {
|
|
1071
1286
|
const result = await api.bill.delete.mutate({ id });
|
|
1072
1287
|
out(result);
|
|
@@ -1576,6 +1791,47 @@ export async function updateProjectStatus(id, status, opts) {
|
|
|
1576
1791
|
});
|
|
1577
1792
|
out(result);
|
|
1578
1793
|
}
|
|
1794
|
+
export const checkProjectReconciliation = async (projectId) => {
|
|
1795
|
+
const result = await api.project.getReconciliationReview.query({ projectId });
|
|
1796
|
+
out(result);
|
|
1797
|
+
};
|
|
1798
|
+
export const reconcileProject = async (projectId) => {
|
|
1799
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1800
|
+
action: "Reconcile project",
|
|
1801
|
+
entity: `project ${projectId}`,
|
|
1802
|
+
details: "rerun all reconciliation checks and mark it RECONCILED",
|
|
1803
|
+
});
|
|
1804
|
+
const result = await api.project.reconcile.mutate({ projectId });
|
|
1805
|
+
out(result);
|
|
1806
|
+
};
|
|
1807
|
+
export const completeProject = async (projectId) => {
|
|
1808
|
+
await updateProjectStatus(projectId, ProjectStatus.COMPLETED);
|
|
1809
|
+
};
|
|
1810
|
+
export const importQuickBooksProjectId = async (projectId, projectUrl) => {
|
|
1811
|
+
const result = await api.project.importQboProjectId.mutate({
|
|
1812
|
+
projectId,
|
|
1813
|
+
projectUrl,
|
|
1814
|
+
});
|
|
1815
|
+
out(result);
|
|
1816
|
+
};
|
|
1817
|
+
export const listIntegrationOperations = async (input) => {
|
|
1818
|
+
const result = await api.integration.listOperations.query(input);
|
|
1819
|
+
out(result);
|
|
1820
|
+
};
|
|
1821
|
+
export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled) => {
|
|
1822
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1823
|
+
action: "Retry integration operation",
|
|
1824
|
+
entity: `operation ${operationId}`,
|
|
1825
|
+
details: confirmExternalStateReconciled
|
|
1826
|
+
? "after confirming the external state has been reconciled"
|
|
1827
|
+
: undefined,
|
|
1828
|
+
});
|
|
1829
|
+
const result = await api.integration.retryOperation.mutate({
|
|
1830
|
+
operationId,
|
|
1831
|
+
confirmExternalStateReconciled,
|
|
1832
|
+
});
|
|
1833
|
+
out(result);
|
|
1834
|
+
};
|
|
1579
1835
|
// --- Contacts ---
|
|
1580
1836
|
export async function listContacts(companyId) {
|
|
1581
1837
|
const contacts = await api.contactPerson.getContactPersonByCompanyId.query({
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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, approveQuotation, approveSupplier, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, 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, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, revokeApiKeyForUser, stageBillAttachmentsFromPaths, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
|
|
5
|
+
import { addBudgetItems, approveBill, approveBudget, approveCustomerInvoice, approveQuotation, approveSupplier, checkCustomerInvoiceReadiness, checkProjectReconciliation, cleanupStagedBillAttachments, cleanupStagedQuotationAttachments, completeProject, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createCustomerInvoice, createEstimate, createItemCategory, createItemFromPayload, createPlaceholderBillForBudgetItem, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteBudgetCommission, deleteBudgetDiscount, deleteCompanyById, deleteCustomerInvoice, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteQuotationById, deleteSuppliersByIds, discardCreatingCustomerInvoice, downloadCustomerInvoicePdf, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getCustomerInvoice, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, importQuickBooksProjectId, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listCustomerInvoices, listIntegrationOperations, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, reconcileProject, rejectBill, rejectBudget, rejectCustomerInvoice, rejectQuotation, rejectSupplier, removeBudgetItem, renameBudgetVersion, reorderBudgetItemsCli, restoreBudgetVersion, retryIntegrationOperation, revokeApiKeyForUser, setBudgetItemsNotUtilized, stageBillAttachmentsFromPaths, submitQuotation, syncCustomerInvoice, updateBillFromPayload, updateBillPaymentEvidenceFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateQuotationFromPayload, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, validateBillSelectionFromPayload, voidCustomerInvoice, } from "./commands.js";
|
|
6
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
|
-
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
|
|
7
|
+
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedCustomerInvoiceStatuses, parseCommaSeparatedIds, parseCommaSeparatedQuotationStatuses, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
|
|
8
8
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
9
9
|
function parsePositiveIntFlag(value, flagName) {
|
|
10
10
|
if (value === undefined)
|
|
@@ -26,6 +26,35 @@ function parseOptionalBoolFlag(flags, key) {
|
|
|
26
26
|
return false;
|
|
27
27
|
throw new Error(`--${key} must be true or false.`);
|
|
28
28
|
}
|
|
29
|
+
const CUSTOMER_INVOICE_SORT_FIELDS = [
|
|
30
|
+
"createdAt",
|
|
31
|
+
"status",
|
|
32
|
+
"totalNetAmount",
|
|
33
|
+
"approvalExpiresAt",
|
|
34
|
+
];
|
|
35
|
+
function parseCustomerInvoiceSortBy(value) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return "createdAt";
|
|
38
|
+
if (!CUSTOMER_INVOICE_SORT_FIELDS.includes(value)) {
|
|
39
|
+
throw new Error(`Invalid --sortBy "${value}". Use one of: ${CUSTOMER_INVOICE_SORT_FIELDS.join(", ")}.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
const INTEGRATION_OPERATION_STATUSES = [
|
|
44
|
+
"PENDING",
|
|
45
|
+
"PROCESSING",
|
|
46
|
+
"FAILED",
|
|
47
|
+
"COMPLETED",
|
|
48
|
+
];
|
|
49
|
+
function parseIntegrationOperationStatus(value) {
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const normalized = value.toUpperCase();
|
|
53
|
+
if (!INTEGRATION_OPERATION_STATUSES.includes(normalized)) {
|
|
54
|
+
throw new Error(`Invalid --status "${value}". Use one of: ${INTEGRATION_OPERATION_STATUSES.join(", ")}.`);
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
29
58
|
function parseCommercialRejectionProjectStatus(flags) {
|
|
30
59
|
const raw = flags.projectStatusOnCommercialRejection;
|
|
31
60
|
if (raw === undefined)
|
|
@@ -120,8 +149,9 @@ Global: --quiet | -q | BB_CLI_QUIET=1 — hide action logs (default: log command
|
|
|
120
149
|
Auth: Set BB_API_KEY in the environment (same API key as MCP / Goracle).
|
|
121
150
|
|
|
122
151
|
Sensitive workflow changes: approval decisions/requests, entity status updates,
|
|
123
|
-
mark-budget-won, submit-quotation, and
|
|
124
|
-
require an interactive CONFIRM prompt
|
|
152
|
+
customer invoices, integration retries, mark-budget-won, submit-quotation, and
|
|
153
|
+
supplier approval request side effects require an interactive CONFIRM prompt
|
|
154
|
+
and abort in non-interactive shells.
|
|
125
155
|
AI agents must inspect the entity first, summarize the exact entity, target
|
|
126
156
|
state, and side effects, then wait for explicit user confirmation before running
|
|
127
157
|
the sensitive command.
|
|
@@ -133,6 +163,8 @@ Budgets
|
|
|
133
163
|
get-budget-details <id> Full detail (margins, suppliers, etc.)
|
|
134
164
|
get-budget-categories
|
|
135
165
|
get-budget-versions <budgetId>
|
|
166
|
+
rename-budget-version <versionId> --name <text>
|
|
167
|
+
restore-budget-version <versionId> Replace current budget data and return it to DRAFT.
|
|
136
168
|
update-budget-status <id> <status> [--projectManagerId <id>] [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
137
169
|
Budget-changing commands require interactive "yes" confirmation when the current budget is ESTIMATE_CREATED, ESTIMATE_SENT, ESTIMATE_ACCEPTED, ESTIMATE_REJECTED, or ESTIMATE_CLOSED.
|
|
138
170
|
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, requires --projectManagerId when the project has no PM, and auto-creates the Asana project/section when enabled.
|
|
@@ -154,6 +186,11 @@ Budgets
|
|
|
154
186
|
delete-budget-category <id>
|
|
155
187
|
update-budget-commission --payload '<json>' (budget.updateBudgetCommission)
|
|
156
188
|
update-budget-discount --payload '<json>' (budget.updateBudgetDiscount; type percentage|amount)
|
|
189
|
+
delete-budget-commission <budgetId> Remove commission and return the budget to DRAFT.
|
|
190
|
+
delete-budget-discount <budgetId> Remove discount and return the budget to DRAFT.
|
|
191
|
+
mark-budget-items-not-utilized --ids <csv> Remove unused lines from reconciliation and clear QBO placeholders.
|
|
192
|
+
restore-budget-item <budgetItemId> Restore one Not Utilized line and its QBO placeholder when required.
|
|
193
|
+
create-placeholder-bill <budgetItemId> Admin-only placeholder recovery.
|
|
157
194
|
upload-budget-attachment <budgetId> <filePath> Presigned S3 upload + confirm
|
|
158
195
|
|
|
159
196
|
Bills
|
|
@@ -163,8 +200,11 @@ Bills
|
|
|
163
200
|
New supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for each non-legacy line item.
|
|
164
201
|
create-bill-approval <billId> (also sends approval request emails)
|
|
165
202
|
create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
|
|
203
|
+
validate-bill-selection --payload '<json>' Check projectId, supplierId, budgetItemIds, and optional alreadyPaid before creation.
|
|
166
204
|
stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
|
|
205
|
+
cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
|
|
167
206
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
207
|
+
update-bill-payment-evidence --payload '<json>' Replace paymentReference/paymentProofAttachments for an already-paid bill.
|
|
168
208
|
delete-bill <billId>
|
|
169
209
|
update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
|
|
170
210
|
status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
|
|
@@ -179,17 +219,33 @@ Bills
|
|
|
179
219
|
get-bill-details <billId>
|
|
180
220
|
|
|
181
221
|
Quotations
|
|
182
|
-
list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
|
|
222
|
+
list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--requesterIds CSV] [--search] [--page] [--pageSize]
|
|
183
223
|
get-quotation <quotationId>
|
|
184
|
-
upload-quotation-attachment <projectId> <filePath>
|
|
224
|
+
upload-quotation-attachment <projectId> <filePath> Upload one PDF, JPEG, or PNG up to 20MB; returns attachment JSON for a quotation payload.
|
|
225
|
+
cleanup-staged-quotation-attachments <projectId> --keys <csv> Delete unattached staged quotation uploads.
|
|
185
226
|
create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and gstAmount)
|
|
227
|
+
update-quotation --payload '<json>' Update a DRAFT or REJECTED quotation; same payload plus id.
|
|
228
|
+
delete-quotation <quotationId>
|
|
186
229
|
submit-quotation <quotationId>
|
|
187
230
|
approve-quotation <quotationId>
|
|
188
231
|
reject-quotation <quotationId> --reason <text>
|
|
189
232
|
download-quotation-pdf <quotationId> [original|staff|final]
|
|
190
233
|
|
|
234
|
+
Customer invoices
|
|
235
|
+
check-customer-invoice-readiness <budgetId>
|
|
236
|
+
list-customer-invoices <budgetId> [--status CSV] [--search] [--sortBy createdAt|status|totalNetAmount|approvalExpiresAt] [--sortDir asc|desc] [--page] [--perPage]
|
|
237
|
+
get-customer-invoice <batchId>
|
|
238
|
+
create-customer-invoice --payload '<json>' budgetId + one split with label, percentage, and dueDate.
|
|
239
|
+
discard-customer-invoice <batchId> Discard a reserved CREATING batch with no created QBO invoices.
|
|
240
|
+
delete-customer-invoice <batchId> Delete QBO invoices and the local batch.
|
|
241
|
+
void-customer-invoice <batchId> Void the invoice batch in QuickBooks.
|
|
242
|
+
approve-customer-invoice <batchId> Admin approval; notifies the creator.
|
|
243
|
+
reject-customer-invoice <batchId> --reason <text> Admin rejection; voids QBO invoices and notifies the creator.
|
|
244
|
+
download-customer-invoice-pdf <invoiceId> [--output <path>]
|
|
245
|
+
sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks.
|
|
246
|
+
|
|
191
247
|
Approvals
|
|
192
|
-
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|all]
|
|
248
|
+
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|customer_invoice|all]
|
|
193
249
|
approve-bill <billId> (also sends requester reply email)
|
|
194
250
|
reject-bill <billId> --reason <text> (also sends requester reply email)
|
|
195
251
|
approve-budget <budgetId> (also sends requester reply email)
|
|
@@ -211,8 +267,12 @@ Companies & projects
|
|
|
211
267
|
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
|
|
212
268
|
update-project --payload '<json>' (project.updateProject; optional projectManagerId and requestQboAccountantNotification; default notify like web)
|
|
213
269
|
delete-project <id>
|
|
270
|
+
check-project-reconciliation <id> Run the same live reconciliation review as the web app without changing status.
|
|
271
|
+
reconcile-project <id> Rerun all checks transactionally and mark an eligible WON project RECONCILED.
|
|
272
|
+
complete-project <id> Rerun reconciliation validation, mark a RECONCILED project COMPLETED, and queue QBO placeholder cleanup.
|
|
273
|
+
import-qbo-project <projectId> --url <quickBooksProjectUrl>
|
|
214
274
|
update-project-status <id> <status> [--projectManagerId <userId>] [--projectManagerEmail <email>] [--wonOverrideReason <text>]
|
|
215
|
-
|
|
275
|
+
Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
|
|
216
276
|
Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
|
|
217
277
|
status: ${projectStatusesForHelp.join(", ")}
|
|
218
278
|
|
|
@@ -261,6 +321,10 @@ Errors
|
|
|
261
321
|
get-recent-errors [--page] [--perPage] [--severity] [--status]
|
|
262
322
|
get-error-metrics
|
|
263
323
|
|
|
324
|
+
Automation (admin)
|
|
325
|
+
list-integration-operations [--destination] [--status PENDING|PROCESSING|FAILED|COMPLETED] [--page] [--perPage]
|
|
326
|
+
retry-integration-operation <operationId> [--confirmExternalStateReconciled true|false]
|
|
327
|
+
|
|
264
328
|
Historical / benchmarks
|
|
265
329
|
get-approved-budgets [--projectId] [--limit] [--minRevenue] [--maxRevenue]
|
|
266
330
|
get-budget-category-benchmarks <categoryId> [--limit]
|
|
@@ -377,6 +441,23 @@ async function main() {
|
|
|
377
441
|
await getBudgetVersions(id);
|
|
378
442
|
break;
|
|
379
443
|
}
|
|
444
|
+
case "rename-budget-version": {
|
|
445
|
+
const versionId = positional[0];
|
|
446
|
+
const newName = getFlag(flags, "name");
|
|
447
|
+
if (!versionId || newName === undefined) {
|
|
448
|
+
throw new Error("rename-budget-version requires <versionId> --name <text>");
|
|
449
|
+
}
|
|
450
|
+
await renameBudgetVersion(versionId, String(newName));
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
case "restore-budget-version": {
|
|
454
|
+
const versionId = positional[0];
|
|
455
|
+
if (!versionId) {
|
|
456
|
+
throw new Error("restore-budget-version requires <versionId>");
|
|
457
|
+
}
|
|
458
|
+
await restoreBudgetVersion(versionId);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
380
461
|
case "update-budget-status": {
|
|
381
462
|
const [id, status] = positional;
|
|
382
463
|
if (!id || !status) {
|
|
@@ -560,6 +641,46 @@ async function main() {
|
|
|
560
641
|
await updateBudgetDiscountFromPayload(raw);
|
|
561
642
|
break;
|
|
562
643
|
}
|
|
644
|
+
case "delete-budget-commission": {
|
|
645
|
+
const budgetId = positional[0];
|
|
646
|
+
if (!budgetId) {
|
|
647
|
+
throw new Error("delete-budget-commission requires <budgetId>");
|
|
648
|
+
}
|
|
649
|
+
await deleteBudgetCommission(budgetId);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case "delete-budget-discount": {
|
|
653
|
+
const budgetId = positional[0];
|
|
654
|
+
if (!budgetId) {
|
|
655
|
+
throw new Error("delete-budget-discount requires <budgetId>");
|
|
656
|
+
}
|
|
657
|
+
await deleteBudgetDiscount(budgetId);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "mark-budget-items-not-utilized": {
|
|
661
|
+
const budgetItemIds = parseCommaSeparatedIds(getFlag(flags, "ids"));
|
|
662
|
+
if (!budgetItemIds?.length) {
|
|
663
|
+
throw new Error("mark-budget-items-not-utilized requires --ids <csv>");
|
|
664
|
+
}
|
|
665
|
+
await setBudgetItemsNotUtilized(budgetItemIds, true);
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case "restore-budget-item": {
|
|
669
|
+
const budgetItemId = positional[0];
|
|
670
|
+
if (!budgetItemId) {
|
|
671
|
+
throw new Error("restore-budget-item requires <budgetItemId>");
|
|
672
|
+
}
|
|
673
|
+
await setBudgetItemsNotUtilized([budgetItemId], false);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
case "create-placeholder-bill": {
|
|
677
|
+
const budgetItemId = positional[0];
|
|
678
|
+
if (!budgetItemId) {
|
|
679
|
+
throw new Error("create-placeholder-bill requires <budgetItemId>");
|
|
680
|
+
}
|
|
681
|
+
await createPlaceholderBillForBudgetItem(budgetItemId);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
563
684
|
case "list-bills": {
|
|
564
685
|
await listBills(parseListBillsFlags(flags));
|
|
565
686
|
break;
|
|
@@ -587,6 +708,15 @@ async function main() {
|
|
|
587
708
|
await createBillFromPayload(raw);
|
|
588
709
|
break;
|
|
589
710
|
}
|
|
711
|
+
case "validate-bill-selection": {
|
|
712
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
713
|
+
if (!payloadRaw) {
|
|
714
|
+
throw new Error("validate-bill-selection requires --payload '<json>'");
|
|
715
|
+
}
|
|
716
|
+
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
717
|
+
await validateBillSelectionFromPayload(raw);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
590
720
|
case "update-bill": {
|
|
591
721
|
const payloadRaw = getFlag(flags, "payload");
|
|
592
722
|
if (!payloadRaw) {
|
|
@@ -596,6 +726,15 @@ async function main() {
|
|
|
596
726
|
await updateBillFromPayload(raw);
|
|
597
727
|
break;
|
|
598
728
|
}
|
|
729
|
+
case "update-bill-payment-evidence": {
|
|
730
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
731
|
+
if (!payloadRaw) {
|
|
732
|
+
throw new Error("update-bill-payment-evidence requires --payload '<json>'");
|
|
733
|
+
}
|
|
734
|
+
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
735
|
+
await updateBillPaymentEvidenceFromPayload(raw);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
599
738
|
case "delete-bill": {
|
|
600
739
|
const id = positional[0];
|
|
601
740
|
if (!id)
|
|
@@ -714,6 +853,15 @@ async function main() {
|
|
|
714
853
|
await stageBillAttachmentsFromPaths(projectId, filePaths);
|
|
715
854
|
break;
|
|
716
855
|
}
|
|
856
|
+
case "cleanup-staged-bill-attachments": {
|
|
857
|
+
const projectId = positional[0];
|
|
858
|
+
const keys = parseCommaSeparatedIds(getFlag(flags, "keys"));
|
|
859
|
+
if (!projectId || !keys) {
|
|
860
|
+
throw new Error("cleanup-staged-bill-attachments requires <projectId> --keys <csv>");
|
|
861
|
+
}
|
|
862
|
+
await cleanupStagedBillAttachments(projectId, keys);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
717
865
|
case "upload-bill-attachment":
|
|
718
866
|
case "attach-bill-attachment":
|
|
719
867
|
case "attach-bill-payment-receipt": {
|
|
@@ -794,12 +942,9 @@ async function main() {
|
|
|
794
942
|
projectId: getFlag(flags, "projectId"),
|
|
795
943
|
budgetId: getFlag(flags, "budgetId"),
|
|
796
944
|
supplierId: getFlag(flags, "supplierId"),
|
|
797
|
-
status: statusRaw
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
.map((value) => value.trim().toUpperCase())
|
|
801
|
-
.filter(Boolean)
|
|
802
|
-
: undefined,
|
|
945
|
+
status: parseCommaSeparatedQuotationStatuses(statusRaw),
|
|
946
|
+
requesterIds: parseCommaSeparatedIds(getFlag(flags, "requesterIds")),
|
|
947
|
+
search: getFlag(flags, "search"),
|
|
803
948
|
page: getFlag(flags, "page") !== undefined
|
|
804
949
|
? Number(getFlag(flags, "page"))
|
|
805
950
|
: undefined,
|
|
@@ -824,6 +969,15 @@ async function main() {
|
|
|
824
969
|
await uploadQuotationAttachmentFromPath(projectId, filePath);
|
|
825
970
|
break;
|
|
826
971
|
}
|
|
972
|
+
case "cleanup-staged-quotation-attachments": {
|
|
973
|
+
const projectId = positional[0];
|
|
974
|
+
const keys = parseCommaSeparatedIds(getFlag(flags, "keys"));
|
|
975
|
+
if (!projectId || !keys) {
|
|
976
|
+
throw new Error("cleanup-staged-quotation-attachments requires <projectId> --keys <csv>");
|
|
977
|
+
}
|
|
978
|
+
await cleanupStagedQuotationAttachments(projectId, keys);
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
827
981
|
case "create-quotation": {
|
|
828
982
|
const payloadRaw = getFlag(flags, "payload");
|
|
829
983
|
if (!payloadRaw) {
|
|
@@ -833,6 +987,22 @@ async function main() {
|
|
|
833
987
|
await createQuotationFromPayload(raw);
|
|
834
988
|
break;
|
|
835
989
|
}
|
|
990
|
+
case "update-quotation": {
|
|
991
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
992
|
+
if (!payloadRaw) {
|
|
993
|
+
throw new Error("update-quotation requires --payload '<json>'");
|
|
994
|
+
}
|
|
995
|
+
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
996
|
+
await updateQuotationFromPayload(raw);
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
case "delete-quotation": {
|
|
1000
|
+
const id = positional[0];
|
|
1001
|
+
if (!id)
|
|
1002
|
+
throw new Error("delete-quotation requires <quotationId>");
|
|
1003
|
+
await deleteQuotationById(id);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
836
1006
|
case "submit-quotation": {
|
|
837
1007
|
const id = positional[0];
|
|
838
1008
|
if (!id)
|
|
@@ -866,10 +1036,114 @@ async function main() {
|
|
|
866
1036
|
? "ORIGINAL"
|
|
867
1037
|
: roleRaw === "staff"
|
|
868
1038
|
? "STAFF_SIGNED"
|
|
869
|
-
: "
|
|
1039
|
+
: roleRaw === "final"
|
|
1040
|
+
? "FINAL_SIGNED"
|
|
1041
|
+
: null;
|
|
1042
|
+
if (!fileRole) {
|
|
1043
|
+
throw new Error("download-quotation-pdf role must be original, staff, or final");
|
|
1044
|
+
}
|
|
870
1045
|
await downloadQuotationPdf(id, fileRole);
|
|
871
1046
|
break;
|
|
872
1047
|
}
|
|
1048
|
+
case "check-customer-invoice-readiness": {
|
|
1049
|
+
const budgetId = positional[0];
|
|
1050
|
+
if (!budgetId) {
|
|
1051
|
+
throw new Error("check-customer-invoice-readiness requires <budgetId>");
|
|
1052
|
+
}
|
|
1053
|
+
await checkCustomerInvoiceReadiness(budgetId);
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
case "list-customer-invoices": {
|
|
1057
|
+
const budgetId = positional[0];
|
|
1058
|
+
if (!budgetId) {
|
|
1059
|
+
throw new Error("list-customer-invoices requires <budgetId>");
|
|
1060
|
+
}
|
|
1061
|
+
const statusRaw = getFlag(flags, "status");
|
|
1062
|
+
const sortDir = parseOptionalBillListSortDir(getFlag(flags, "sortDir"));
|
|
1063
|
+
await listCustomerInvoices({
|
|
1064
|
+
budgetId,
|
|
1065
|
+
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
|
|
1066
|
+
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 12,
|
|
1067
|
+
q: getFlag(flags, "search") ?? "",
|
|
1068
|
+
statuses: parseCommaSeparatedCustomerInvoiceStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
|
|
1069
|
+
sortBy: parseCustomerInvoiceSortBy(getFlag(flags, "sortBy")),
|
|
1070
|
+
sortDir: sortDir ?? "desc",
|
|
1071
|
+
});
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case "get-customer-invoice": {
|
|
1075
|
+
const batchId = positional[0];
|
|
1076
|
+
if (!batchId) {
|
|
1077
|
+
throw new Error("get-customer-invoice requires <batchId>");
|
|
1078
|
+
}
|
|
1079
|
+
await getCustomerInvoice(batchId);
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
case "create-customer-invoice": {
|
|
1083
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
1084
|
+
if (!payloadRaw) {
|
|
1085
|
+
throw new Error("create-customer-invoice requires --payload '<json>'");
|
|
1086
|
+
}
|
|
1087
|
+
await createCustomerInvoice(parseJsonFlag(String(payloadRaw), "--payload"));
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
case "discard-customer-invoice": {
|
|
1091
|
+
const batchId = positional[0];
|
|
1092
|
+
if (!batchId) {
|
|
1093
|
+
throw new Error("discard-customer-invoice requires <batchId>");
|
|
1094
|
+
}
|
|
1095
|
+
await discardCreatingCustomerInvoice(batchId);
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
case "delete-customer-invoice": {
|
|
1099
|
+
const batchId = positional[0];
|
|
1100
|
+
if (!batchId) {
|
|
1101
|
+
throw new Error("delete-customer-invoice requires <batchId>");
|
|
1102
|
+
}
|
|
1103
|
+
await deleteCustomerInvoice(batchId);
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
case "void-customer-invoice": {
|
|
1107
|
+
const batchId = positional[0];
|
|
1108
|
+
if (!batchId) {
|
|
1109
|
+
throw new Error("void-customer-invoice requires <batchId>");
|
|
1110
|
+
}
|
|
1111
|
+
await voidCustomerInvoice(batchId);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
case "approve-customer-invoice": {
|
|
1115
|
+
const batchId = positional[0];
|
|
1116
|
+
if (!batchId) {
|
|
1117
|
+
throw new Error("approve-customer-invoice requires <batchId>");
|
|
1118
|
+
}
|
|
1119
|
+
await approveCustomerInvoice(batchId);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
case "reject-customer-invoice": {
|
|
1123
|
+
const batchId = positional[0];
|
|
1124
|
+
const reason = getFlag(flags, "reason");
|
|
1125
|
+
if (!batchId || reason === undefined) {
|
|
1126
|
+
throw new Error("reject-customer-invoice requires <batchId> --reason <text>");
|
|
1127
|
+
}
|
|
1128
|
+
await rejectCustomerInvoice(batchId, String(reason));
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case "download-customer-invoice-pdf": {
|
|
1132
|
+
const invoiceId = positional[0];
|
|
1133
|
+
if (!invoiceId) {
|
|
1134
|
+
throw new Error("download-customer-invoice-pdf requires <invoiceId> [--output <path>]");
|
|
1135
|
+
}
|
|
1136
|
+
await downloadCustomerInvoicePdf(invoiceId, getFlag(flags, "output"));
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
case "sync-customer-invoice": {
|
|
1140
|
+
const invoiceId = positional[0];
|
|
1141
|
+
if (!invoiceId) {
|
|
1142
|
+
throw new Error("sync-customer-invoice requires <invoiceId>");
|
|
1143
|
+
}
|
|
1144
|
+
await syncCustomerInvoice(invoiceId);
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
873
1147
|
case "list-approvals":
|
|
874
1148
|
case "get-pending-approvals": {
|
|
875
1149
|
const typeRaw = getFlag(flags, "type");
|
|
@@ -1057,6 +1331,37 @@ async function main() {
|
|
|
1057
1331
|
await deleteProjectById(id);
|
|
1058
1332
|
break;
|
|
1059
1333
|
}
|
|
1334
|
+
case "check-project-reconciliation": {
|
|
1335
|
+
const id = positional[0];
|
|
1336
|
+
if (!id) {
|
|
1337
|
+
throw new Error("check-project-reconciliation requires <id>");
|
|
1338
|
+
}
|
|
1339
|
+
await checkProjectReconciliation(id);
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
case "reconcile-project": {
|
|
1343
|
+
const id = positional[0];
|
|
1344
|
+
if (!id)
|
|
1345
|
+
throw new Error("reconcile-project requires <id>");
|
|
1346
|
+
await reconcileProject(id);
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
case "complete-project": {
|
|
1350
|
+
const id = positional[0];
|
|
1351
|
+
if (!id)
|
|
1352
|
+
throw new Error("complete-project requires <id>");
|
|
1353
|
+
await completeProject(id);
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
case "import-qbo-project": {
|
|
1357
|
+
const projectId = positional[0];
|
|
1358
|
+
const projectUrl = getFlag(flags, "url");
|
|
1359
|
+
if (!projectId || projectUrl === undefined) {
|
|
1360
|
+
throw new Error("import-qbo-project requires <projectId> --url <quickBooksProjectUrl>");
|
|
1361
|
+
}
|
|
1362
|
+
await importQuickBooksProjectId(projectId, String(projectUrl));
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1060
1365
|
case "update-project-status": {
|
|
1061
1366
|
const [id, statusRaw] = positional;
|
|
1062
1367
|
if (!id || !statusRaw) {
|
|
@@ -1344,6 +1649,24 @@ async function main() {
|
|
|
1344
1649
|
await getErrorMetrics();
|
|
1345
1650
|
break;
|
|
1346
1651
|
}
|
|
1652
|
+
case "list-integration-operations": {
|
|
1653
|
+
await listIntegrationOperations({
|
|
1654
|
+
destination: getFlag(flags, "destination"),
|
|
1655
|
+
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page") ?? 1,
|
|
1656
|
+
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage") ?? 25,
|
|
1657
|
+
status: parseIntegrationOperationStatus(getFlag(flags, "status")),
|
|
1658
|
+
});
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
case "retry-integration-operation": {
|
|
1662
|
+
const operationId = positional[0];
|
|
1663
|
+
if (!operationId) {
|
|
1664
|
+
throw new Error("retry-integration-operation requires <operationId>");
|
|
1665
|
+
}
|
|
1666
|
+
await retryIntegrationOperation(operationId, parseOptionalBoolFlag(flags, "confirmExternalStateReconciled") ??
|
|
1667
|
+
false);
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1347
1670
|
case "get-approved-budgets": {
|
|
1348
1671
|
await getApprovedBudgets({
|
|
1349
1672
|
projectId: getFlag(flags, "projectId"),
|
package/dist/parse-cli-enums.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
|
|
2
|
-
import { BillStatus, BudgetStatus, ProjectStatus, UserRole, } from "./prisma-enums.js";
|
|
2
|
+
import { BillStatus, BudgetStatus, CustomerInvoiceBatchStatus, ProjectStatus, SupplierQuotationStatus, UserRole, } from "./prisma-enums.js";
|
|
3
3
|
const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
|
|
4
4
|
const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
|
|
5
5
|
const PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus));
|
|
6
|
+
const SUPPLIER_QUOTATION_STATUS_VALUES = new Set(Object.values(SupplierQuotationStatus));
|
|
7
|
+
const CUSTOMER_INVOICE_BATCH_STATUS_VALUES = new Set(Object.values(CustomerInvoiceBatchStatus));
|
|
6
8
|
const USER_ROLE_VALUES = new Set(Object.values(UserRole));
|
|
7
9
|
const EXTENDED_PROJECT_STATUS_VALUES = new Set(Object.values(ExtendedProjectStatus));
|
|
8
10
|
const DASHBOARD_ROLE_VALUES = new Set([
|
|
@@ -173,6 +175,38 @@ export function parseCommaSeparatedSupplierStatuses(raw) {
|
|
|
173
175
|
}
|
|
174
176
|
return out;
|
|
175
177
|
}
|
|
178
|
+
export function parseCommaSeparatedQuotationStatuses(raw) {
|
|
179
|
+
if (raw === undefined)
|
|
180
|
+
return undefined;
|
|
181
|
+
const parts = raw
|
|
182
|
+
.split(",")
|
|
183
|
+
.map((value) => value.trim().toUpperCase())
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
if (parts.length === 0)
|
|
186
|
+
return undefined;
|
|
187
|
+
for (const status of parts) {
|
|
188
|
+
if (!SUPPLIER_QUOTATION_STATUS_VALUES.has(status)) {
|
|
189
|
+
throw new Error(`Invalid quotation status "${status}". Use one of: ${[...SUPPLIER_QUOTATION_STATUS_VALUES].join(", ")}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return parts;
|
|
193
|
+
}
|
|
194
|
+
export function parseCommaSeparatedCustomerInvoiceStatuses(raw) {
|
|
195
|
+
if (raw === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
const parts = raw
|
|
198
|
+
.split(",")
|
|
199
|
+
.map((value) => value.trim().toUpperCase())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
if (parts.length === 0)
|
|
202
|
+
return undefined;
|
|
203
|
+
for (const status of parts) {
|
|
204
|
+
if (!CUSTOMER_INVOICE_BATCH_STATUS_VALUES.has(status)) {
|
|
205
|
+
throw new Error(`Invalid customer invoice status "${status}". Use one of: ${[...CUSTOMER_INVOICE_BATCH_STATUS_VALUES].join(", ")}.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return parts;
|
|
209
|
+
}
|
|
176
210
|
export function parseBillStatusFlag(raw) {
|
|
177
211
|
if (raw === undefined)
|
|
178
212
|
return undefined;
|
|
@@ -225,7 +259,11 @@ export function parseOptionalBillListSortDir(raw) {
|
|
|
225
259
|
return raw;
|
|
226
260
|
}
|
|
227
261
|
function isPendingRequestType(s) {
|
|
228
|
-
return (s === "BUDGET" ||
|
|
262
|
+
return (s === "BUDGET" ||
|
|
263
|
+
s === "BILL" ||
|
|
264
|
+
s === "SUPPLIER" ||
|
|
265
|
+
s === "QUOTATION" ||
|
|
266
|
+
s === "CUSTOMER_INVOICE");
|
|
229
267
|
}
|
|
230
268
|
export function parseApprovalTypeFlag(raw) {
|
|
231
269
|
if (raw === undefined)
|
|
@@ -234,7 +272,7 @@ export function parseApprovalTypeFlag(raw) {
|
|
|
234
272
|
if (u === "ALL")
|
|
235
273
|
return "ALL";
|
|
236
274
|
if (!isPendingRequestType(u)) {
|
|
237
|
-
throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, quotation, or all.`);
|
|
275
|
+
throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, quotation, customer_invoice, or all.`);
|
|
238
276
|
}
|
|
239
277
|
return u;
|
|
240
278
|
}
|
|
@@ -190,6 +190,42 @@ export function parseUpdateBillPayload(raw) {
|
|
|
190
190
|
id: requiredString(o.id, "update-bill payload.id"),
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
+
/** Server performs the authoritative bill-selection and quotation-coverage checks. */
|
|
194
|
+
export function parseValidateBillSelectionPayload(raw) {
|
|
195
|
+
requireObject(raw, "validate-bill-selection payload");
|
|
196
|
+
return raw;
|
|
197
|
+
}
|
|
198
|
+
/** Matches bill.updatePaymentEvidence for already-paid supplier bills. */
|
|
199
|
+
export function parseUpdateBillPaymentEvidencePayload(raw) {
|
|
200
|
+
requireObject(raw, "update-bill-payment-evidence payload");
|
|
201
|
+
return raw;
|
|
202
|
+
}
|
|
203
|
+
/** Server validates quotation amounts, line items, and the single attachment. */
|
|
204
|
+
export function parseCreateQuotationPayload(raw) {
|
|
205
|
+
requireObject(raw, "create-quotation payload");
|
|
206
|
+
return raw;
|
|
207
|
+
}
|
|
208
|
+
export function parseUpdateQuotationPayload(raw) {
|
|
209
|
+
requireObject(raw, "update-quotation payload");
|
|
210
|
+
return raw;
|
|
211
|
+
}
|
|
212
|
+
export function parseCreateCustomerInvoicePayload(raw) {
|
|
213
|
+
const input = requireObject(raw, "create-customer-invoice payload");
|
|
214
|
+
if (!Array.isArray(input.splits) || input.splits.length === 0) {
|
|
215
|
+
throw new Error("create-customer-invoice payload.splits must be a non-empty array.");
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
budgetId: requiredString(input.budgetId, "create-customer-invoice payload.budgetId"),
|
|
219
|
+
splits: input.splits.map((rawSplit, index) => {
|
|
220
|
+
const split = requireObject(rawSplit, `create-customer-invoice payload.splits[${index}]`);
|
|
221
|
+
return {
|
|
222
|
+
dueDate: toDate(split.dueDate, `create-customer-invoice payload.splits[${index}].dueDate`),
|
|
223
|
+
label: requiredString(split.label, `create-customer-invoice payload.splits[${index}].label`),
|
|
224
|
+
percentage: Number(split.percentage),
|
|
225
|
+
};
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
193
229
|
/** Matches project.updateProject — dateRange.from/to may be ISO strings. */
|
|
194
230
|
export function parseUpdateProjectPayload(raw) {
|
|
195
231
|
const o = requireObject(raw, "update-project payload");
|
package/dist/prisma-enums.js
CHANGED
|
@@ -10,8 +10,24 @@ export const ProjectStatus = {
|
|
|
10
10
|
PITCH: "PITCH",
|
|
11
11
|
WON: "WON",
|
|
12
12
|
COMPLETED: "COMPLETED",
|
|
13
|
+
RECONCILED: "RECONCILED",
|
|
13
14
|
LOST: "LOST",
|
|
14
15
|
};
|
|
16
|
+
export const SupplierQuotationStatus = {
|
|
17
|
+
DRAFT: "DRAFT",
|
|
18
|
+
PENDING_APPROVAL: "PENDING_APPROVAL",
|
|
19
|
+
APPROVED: "APPROVED",
|
|
20
|
+
REJECTED: "REJECTED",
|
|
21
|
+
SUPERSEDED: "SUPERSEDED",
|
|
22
|
+
};
|
|
23
|
+
export const CustomerInvoiceBatchStatus = {
|
|
24
|
+
CREATING: "CREATING",
|
|
25
|
+
PENDING_APPROVAL: "PENDING_APPROVAL",
|
|
26
|
+
APPROVED: "APPROVED",
|
|
27
|
+
REJECTED_VOIDED: "REJECTED_VOIDED",
|
|
28
|
+
EXPIRED_VOIDED: "EXPIRED_VOIDED",
|
|
29
|
+
PARTIAL_QBO_FAILURE: "PARTIAL_QBO_FAILURE",
|
|
30
|
+
};
|
|
15
31
|
export const BudgetStatus = {
|
|
16
32
|
DRAFT: "DRAFT",
|
|
17
33
|
PENDING_APPROVAL: "PENDING_APPROVAL",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.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",
|