@go-labs-sg/bb 1.12.0 → 1.14.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 +4 -3
- package/dist/commands.js +99 -1
- package/dist/index.js +96 -3
- package/dist/parse-cli-enums.js +2 -2
- package/dist/parse-mutation-payload.js +13 -4
- package/dist/prisma-enums.js +2 -0
- package/dist/rich-text.js +34 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,15 +56,16 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
56
56
|
|
|
57
57
|
**Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. If the parent project is `PITCH` or `LOST`, the API now marks it `WON` automatically and, when the `asana-tasks` feature flag is enabled, creates the Asana project/section/tasks automatically. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
|
|
58
58
|
|
|
59
|
-
**Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
|
|
59
|
+
**Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. Plain `description` fields for item create/update are converted to `descriptionRichText`; pass `descriptionRichText` directly when formatted Tiptap JSON is required. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
|
|
60
60
|
|
|
61
61
|
### Command overview
|
|
62
62
|
|
|
63
63
|
| Area | Commands (non-exhaustive) |
|
|
64
64
|
| --- | --- |
|
|
65
65
|
| **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates Asana project/section/tasks when enabled; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
66
|
-
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
67
|
-
| **
|
|
66
|
+
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
67
|
+
| **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
68
|
+
| **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation` |
|
|
68
69
|
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; optional `--pax`; optional `--endDate`; optional `--description`; optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
|
|
69
70
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
70
71
|
| **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) |
|
package/dist/commands.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { basename } from "node:path";
|
|
3
4
|
import { contentType } from "mime-types";
|
|
@@ -5,9 +6,11 @@ import { api } from "./api-client.js";
|
|
|
5
6
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
|
|
6
7
|
import { billStatusesForApi, } from "./parse-cli-enums.js";
|
|
7
8
|
import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, } from "./parse-mutation-payload.js";
|
|
9
|
+
import { createRichTextFromPlainText } from "./rich-text.js";
|
|
8
10
|
const BUDGET = "BUDGET";
|
|
9
11
|
const BILL = "BILL";
|
|
10
12
|
const SUPPLIER = "SUPPLIER";
|
|
13
|
+
const QUOTATION = "QUOTATION";
|
|
11
14
|
function isPendingRequest(n) {
|
|
12
15
|
return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
|
|
13
16
|
}
|
|
@@ -198,6 +201,10 @@ export async function listApprovals(opts) {
|
|
|
198
201
|
id = n.supplier.id;
|
|
199
202
|
name = n.supplier.name;
|
|
200
203
|
}
|
|
204
|
+
else if (n.type === QUOTATION && n.supplierQuotation) {
|
|
205
|
+
id = n.supplierQuotation.id;
|
|
206
|
+
name = `Quotation ${n.supplierQuotation.supplier.name}`;
|
|
207
|
+
}
|
|
201
208
|
else {
|
|
202
209
|
id = n.id;
|
|
203
210
|
name = "Unknown";
|
|
@@ -322,6 +329,33 @@ export async function uploadBillAttachmentFromPath(billId, filePath) {
|
|
|
322
329
|
});
|
|
323
330
|
out(confirmed);
|
|
324
331
|
}
|
|
332
|
+
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
333
|
+
const buf = await readFile(filePath);
|
|
334
|
+
const fileName = basename(filePath);
|
|
335
|
+
const ext = fileName
|
|
336
|
+
.split(".")
|
|
337
|
+
.pop()
|
|
338
|
+
?.toLowerCase()
|
|
339
|
+
.replace(/[^a-z0-9]/g, "");
|
|
340
|
+
const key = `quotations/${projectId}/${randomUUID()}.${ext || "pdf"}`;
|
|
341
|
+
const uploadUrl = await api.attachment.getPresignedUrlToUpload.mutate({
|
|
342
|
+
key,
|
|
343
|
+
});
|
|
344
|
+
const res = await fetch(uploadUrl, {
|
|
345
|
+
method: "PUT",
|
|
346
|
+
body: buf,
|
|
347
|
+
headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
|
|
348
|
+
});
|
|
349
|
+
if (!res.ok) {
|
|
350
|
+
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
351
|
+
}
|
|
352
|
+
out({
|
|
353
|
+
id: randomUUID(),
|
|
354
|
+
name: fileName,
|
|
355
|
+
key,
|
|
356
|
+
size: buf.byteLength,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
325
359
|
export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
326
360
|
const buf = await readFile(filePath);
|
|
327
361
|
const fileName = basename(filePath);
|
|
@@ -396,7 +430,17 @@ export async function addBudgetItems(opts) {
|
|
|
396
430
|
out(result);
|
|
397
431
|
}
|
|
398
432
|
export async function updateBudgetItem(input) {
|
|
399
|
-
const result = await api.budgetItem.updateBudgetItem.mutate(
|
|
433
|
+
const result = await api.budgetItem.updateBudgetItem.mutate({
|
|
434
|
+
...input,
|
|
435
|
+
...(input.description !== undefined &&
|
|
436
|
+
input.descriptionRichText === undefined && {
|
|
437
|
+
descriptionRichText: createRichTextFromPlainText(input.description),
|
|
438
|
+
}),
|
|
439
|
+
...(input.note !== undefined &&
|
|
440
|
+
input.noteRichText === undefined && {
|
|
441
|
+
noteRichText: createRichTextFromPlainText(input.note),
|
|
442
|
+
}),
|
|
443
|
+
});
|
|
400
444
|
out(result);
|
|
401
445
|
}
|
|
402
446
|
export async function removeBudgetItem(budgetItemId) {
|
|
@@ -557,6 +601,29 @@ export async function createBillFromPayload(raw) {
|
|
|
557
601
|
const result = await api.bill.create.mutate(input);
|
|
558
602
|
out(result);
|
|
559
603
|
}
|
|
604
|
+
export async function listQuotations(opts) {
|
|
605
|
+
const result = await api.quotation.list.query(opts);
|
|
606
|
+
out(result);
|
|
607
|
+
}
|
|
608
|
+
export async function getQuotationDetails(id) {
|
|
609
|
+
const result = await api.quotation.getById.query({ id });
|
|
610
|
+
out(result);
|
|
611
|
+
}
|
|
612
|
+
export async function createQuotationFromPayload(raw) {
|
|
613
|
+
const result = await api.quotation.createDraft.mutate(raw);
|
|
614
|
+
out(result);
|
|
615
|
+
}
|
|
616
|
+
export async function submitQuotation(id) {
|
|
617
|
+
const result = await api.quotation.submitForApproval.mutate({ id });
|
|
618
|
+
out(result);
|
|
619
|
+
}
|
|
620
|
+
export async function downloadQuotationPdf(id, fileRole) {
|
|
621
|
+
const result = await api.quotation.getSignedDownloadUrl.query({
|
|
622
|
+
id,
|
|
623
|
+
fileRole,
|
|
624
|
+
});
|
|
625
|
+
out(result);
|
|
626
|
+
}
|
|
560
627
|
export async function updateBillFromPayload(raw) {
|
|
561
628
|
const input = parseUpdateBillPayload(raw);
|
|
562
629
|
const result = await api.bill.update.mutate(input);
|
|
@@ -913,6 +980,37 @@ export async function rejectBill(billId, reason) {
|
|
|
913
980
|
email,
|
|
914
981
|
});
|
|
915
982
|
}
|
|
983
|
+
export async function approveQuotation(quotationId) {
|
|
984
|
+
const notifications = await api.budget.getNotifications.query();
|
|
985
|
+
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
986
|
+
n.type === QUOTATION &&
|
|
987
|
+
(n.supplierQuotationId === quotationId ||
|
|
988
|
+
n.supplierQuotation?.id === quotationId));
|
|
989
|
+
const approval = pending[0];
|
|
990
|
+
if (!approval) {
|
|
991
|
+
throw new Error("No pending quotation approval found.");
|
|
992
|
+
}
|
|
993
|
+
const result = await api.quotation.approve.mutate({
|
|
994
|
+
quotationApprovalId: approval.id,
|
|
995
|
+
});
|
|
996
|
+
out(result);
|
|
997
|
+
}
|
|
998
|
+
export async function rejectQuotation(quotationId, reason) {
|
|
999
|
+
const notifications = await api.budget.getNotifications.query();
|
|
1000
|
+
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1001
|
+
n.type === QUOTATION &&
|
|
1002
|
+
(n.supplierQuotationId === quotationId ||
|
|
1003
|
+
n.supplierQuotation?.id === quotationId));
|
|
1004
|
+
const approval = pending[0];
|
|
1005
|
+
if (!approval) {
|
|
1006
|
+
throw new Error("No pending quotation approval found.");
|
|
1007
|
+
}
|
|
1008
|
+
const result = await api.quotation.reject.mutate({
|
|
1009
|
+
quotationApprovalId: approval.id,
|
|
1010
|
+
rejectionReason: reason,
|
|
1011
|
+
});
|
|
1012
|
+
out(result);
|
|
1013
|
+
}
|
|
916
1014
|
// --- Companies & projects ---
|
|
917
1015
|
export async function listCompanies(opts) {
|
|
918
1016
|
const result = await api.company.getCompanies.query({
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "./load-env.js";
|
|
3
3
|
import { requireApiKey } from "./api-client.js";
|
|
4
4
|
import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
|
|
5
|
-
import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
|
|
5
|
+
import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
|
|
6
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
7
|
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
8
8
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
@@ -137,7 +137,7 @@ Budgets
|
|
|
137
137
|
create-budget-approval <budgetId> (also sends approval request emails)
|
|
138
138
|
create-estimate <budgetId> (creates QuickBooks estimate; supports preflight updates)
|
|
139
139
|
add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
|
|
140
|
-
update-budget-item --id <budgetItemId> [--description] [--note] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
|
|
140
|
+
update-budget-item --id <budgetItemId> [--description plain-text] [--note plain-text] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
|
|
141
141
|
remove-budget-item <budgetItemId>
|
|
142
142
|
reorder-budget-items <budgetId> --itemIds <csv>
|
|
143
143
|
update-budget-item-supplier --budgetItemId --supplierId
|
|
@@ -152,6 +152,7 @@ Bills
|
|
|
152
152
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
153
153
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
154
154
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
155
|
+
New supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for each non-legacy line item.
|
|
155
156
|
create-bill-approval <billId> (also sends approval request emails)
|
|
156
157
|
create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; use upload-bill-attachment after create to add files)
|
|
157
158
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
@@ -165,8 +166,18 @@ Bills
|
|
|
165
166
|
upload-bill-attachment <billId> <filePath> Presigned S3 upload + confirm (same flow as web UI)
|
|
166
167
|
get-bill-details <billId>
|
|
167
168
|
|
|
169
|
+
Quotations
|
|
170
|
+
list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
|
|
171
|
+
get-quotation <quotationId>
|
|
172
|
+
upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
|
|
173
|
+
create-quotation --payload '<json>' (quotation.createDraft)
|
|
174
|
+
submit-quotation <quotationId>
|
|
175
|
+
approve-quotation <quotationId>
|
|
176
|
+
reject-quotation <quotationId> --reason <text>
|
|
177
|
+
download-quotation-pdf <quotationId> [original|staff|final]
|
|
178
|
+
|
|
168
179
|
Approvals
|
|
169
|
-
list-approvals | get-pending-approvals [--type budget|supplier|bill|all]
|
|
180
|
+
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|all]
|
|
170
181
|
approve-bill <billId> (also sends requester reply email)
|
|
171
182
|
reject-bill <billId> --reason <text> (also sends requester reply email)
|
|
172
183
|
approve-budget <budgetId> (also sends requester reply email)
|
|
@@ -674,6 +685,88 @@ async function main() {
|
|
|
674
685
|
await getBillDetails(id);
|
|
675
686
|
break;
|
|
676
687
|
}
|
|
688
|
+
case "list-quotations": {
|
|
689
|
+
const statusRaw = getFlag(flags, "status");
|
|
690
|
+
await listQuotations({
|
|
691
|
+
projectId: getFlag(flags, "projectId"),
|
|
692
|
+
budgetId: getFlag(flags, "budgetId"),
|
|
693
|
+
supplierId: getFlag(flags, "supplierId"),
|
|
694
|
+
status: statusRaw !== undefined
|
|
695
|
+
? String(statusRaw)
|
|
696
|
+
.split(",")
|
|
697
|
+
.map((value) => value.trim().toUpperCase())
|
|
698
|
+
.filter(Boolean)
|
|
699
|
+
: undefined,
|
|
700
|
+
page: getFlag(flags, "page") !== undefined
|
|
701
|
+
? Number(getFlag(flags, "page"))
|
|
702
|
+
: undefined,
|
|
703
|
+
pageSize: getFlag(flags, "pageSize") !== undefined
|
|
704
|
+
? Number(getFlag(flags, "pageSize"))
|
|
705
|
+
: undefined,
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
case "get-quotation": {
|
|
710
|
+
const id = positional[0];
|
|
711
|
+
if (!id)
|
|
712
|
+
throw new Error("get-quotation requires <quotationId>");
|
|
713
|
+
await getQuotationDetails(id);
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case "upload-quotation-attachment": {
|
|
717
|
+
const [projectId, filePath] = positional;
|
|
718
|
+
if (!projectId || !filePath) {
|
|
719
|
+
throw new Error("upload-quotation-attachment requires <projectId> <filePath>");
|
|
720
|
+
}
|
|
721
|
+
await uploadQuotationAttachmentFromPath(projectId, filePath);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "create-quotation": {
|
|
725
|
+
const payloadRaw = getFlag(flags, "payload");
|
|
726
|
+
if (!payloadRaw) {
|
|
727
|
+
throw new Error("create-quotation requires --payload '<json>'");
|
|
728
|
+
}
|
|
729
|
+
const raw = parseJsonFlag(String(payloadRaw), "--payload");
|
|
730
|
+
await createQuotationFromPayload(raw);
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case "submit-quotation": {
|
|
734
|
+
const id = positional[0];
|
|
735
|
+
if (!id)
|
|
736
|
+
throw new Error("submit-quotation requires <quotationId>");
|
|
737
|
+
await submitQuotation(id);
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case "approve-quotation": {
|
|
741
|
+
const id = positional[0];
|
|
742
|
+
if (!id)
|
|
743
|
+
throw new Error("approve-quotation requires <quotationId>");
|
|
744
|
+
await approveQuotation(id);
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "reject-quotation": {
|
|
748
|
+
const id = positional[0];
|
|
749
|
+
const reason = getFlag(flags, "reason");
|
|
750
|
+
if (!id || reason === undefined) {
|
|
751
|
+
throw new Error("reject-quotation requires <quotationId> --reason <text>");
|
|
752
|
+
}
|
|
753
|
+
await rejectQuotation(id, reason);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case "download-quotation-pdf": {
|
|
757
|
+
const id = positional[0];
|
|
758
|
+
if (!id) {
|
|
759
|
+
throw new Error("download-quotation-pdf requires <quotationId> [original|staff|final]");
|
|
760
|
+
}
|
|
761
|
+
const roleRaw = (positional[1] ?? "final").toLowerCase();
|
|
762
|
+
const fileRole = roleRaw === "original"
|
|
763
|
+
? "ORIGINAL"
|
|
764
|
+
: roleRaw === "staff"
|
|
765
|
+
? "STAFF_SIGNED"
|
|
766
|
+
: "FINAL_SIGNED";
|
|
767
|
+
await downloadQuotationPdf(id, fileRole);
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
677
770
|
case "list-approvals":
|
|
678
771
|
case "get-pending-approvals": {
|
|
679
772
|
const typeRaw = getFlag(flags, "type");
|
package/dist/parse-cli-enums.js
CHANGED
|
@@ -215,7 +215,7 @@ export function parseOptionalBillListSortDir(raw) {
|
|
|
215
215
|
return raw;
|
|
216
216
|
}
|
|
217
217
|
function isPendingRequestType(s) {
|
|
218
|
-
return s === "BUDGET" || s === "BILL" || s === "SUPPLIER";
|
|
218
|
+
return (s === "BUDGET" || s === "BILL" || s === "SUPPLIER" || s === "QUOTATION");
|
|
219
219
|
}
|
|
220
220
|
export function parseApprovalTypeFlag(raw) {
|
|
221
221
|
if (raw === undefined)
|
|
@@ -224,7 +224,7 @@ export function parseApprovalTypeFlag(raw) {
|
|
|
224
224
|
if (u === "ALL")
|
|
225
225
|
return "ALL";
|
|
226
226
|
if (!isPendingRequestType(u)) {
|
|
227
|
-
throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, or all.`);
|
|
227
|
+
throw new Error(`Invalid --type "${raw}". Use budget, supplier, bill, quotation, or all.`);
|
|
228
228
|
}
|
|
229
229
|
return u;
|
|
230
230
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizePlainTextRichTextField } from "./rich-text.js";
|
|
1
2
|
function requireObject(raw, label) {
|
|
2
3
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3
4
|
throw new Error(`${label} must be a JSON object.`);
|
|
@@ -237,12 +238,20 @@ export function parseSupplierUpdatePayload(raw) {
|
|
|
237
238
|
return raw;
|
|
238
239
|
}
|
|
239
240
|
export function parseItemCreatePayload(raw) {
|
|
240
|
-
requireObject(raw, "create-item payload");
|
|
241
|
-
return
|
|
241
|
+
const input = requireObject(raw, "create-item payload");
|
|
242
|
+
return normalizePlainTextRichTextField({
|
|
243
|
+
input,
|
|
244
|
+
textField: "description",
|
|
245
|
+
richTextField: "descriptionRichText",
|
|
246
|
+
});
|
|
242
247
|
}
|
|
243
248
|
export function parseItemUpdatePayload(raw) {
|
|
244
|
-
requireObject(raw, "update-item payload");
|
|
245
|
-
return
|
|
249
|
+
const input = requireObject(raw, "update-item payload");
|
|
250
|
+
return normalizePlainTextRichTextField({
|
|
251
|
+
input,
|
|
252
|
+
textField: "description",
|
|
253
|
+
richTextField: "descriptionRichText",
|
|
254
|
+
});
|
|
246
255
|
}
|
|
247
256
|
export function parseContactCreatePayload(raw) {
|
|
248
257
|
requireObject(raw, "create-contact-person payload");
|
package/dist/prisma-enums.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const escapeHtml = (text) => text
|
|
2
|
+
.replace(/&/g, "&")
|
|
3
|
+
.replace(/</g, "<")
|
|
4
|
+
.replace(/>/g, ">")
|
|
5
|
+
.replace(/"/g, """)
|
|
6
|
+
.replace(/'/g, "'");
|
|
7
|
+
export const createRichTextFromPlainText = (text) => {
|
|
8
|
+
const lines = text?.split(/\r?\n/) ?? [];
|
|
9
|
+
const paragraphs = lines.length ? lines : [""];
|
|
10
|
+
return {
|
|
11
|
+
json: {
|
|
12
|
+
type: "doc",
|
|
13
|
+
content: paragraphs.map((line) => ({
|
|
14
|
+
type: "paragraph",
|
|
15
|
+
...(line ? { content: [{ type: "text", text: line }] } : {}),
|
|
16
|
+
})),
|
|
17
|
+
},
|
|
18
|
+
html: paragraphs.map((line) => `<p>${escapeHtml(line)}</p>`).join(""),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export const isRichTextJson = (value) => Boolean(value &&
|
|
22
|
+
typeof value === "object" &&
|
|
23
|
+
"json" in value &&
|
|
24
|
+
"html" in value &&
|
|
25
|
+
typeof value.html === "string");
|
|
26
|
+
export const normalizePlainTextRichTextField = ({ input, textField, richTextField, }) => {
|
|
27
|
+
if (isRichTextJson(input[richTextField]) || input[textField] === undefined) {
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...input,
|
|
32
|
+
[richTextField]: createRichTextFromPlainText(String(input[textField] ?? "")),
|
|
33
|
+
};
|
|
34
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"postpack": "node scripts/restore-package-json.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@trpc/client": "^11.
|
|
24
|
+
"@trpc/client": "^11.18.0",
|
|
25
25
|
"dotenv": "^17.4.2",
|
|
26
26
|
"mime-types": "^3.0.2",
|
|
27
27
|
"superjson": "^2.2.6"
|