@go-labs-sg/bb 1.6.1 → 1.8.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 +8 -4
- package/dist/commands.js +86 -10
- package/dist/index.js +96 -19
- package/dist/parse-cli-enums.js +20 -1
- package/dist/parse-mutation-payload.js +1 -1
- package/dist/prisma-enums.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Command-line interface for **Budget Builder**. Call budgets, bills, claims, appr
|
|
|
4
4
|
|
|
5
5
|
For AI agents: bills and claims are stored as the same underlying bill records. Use the `isClaimable` column/flag to distinguish them: `isClaimable=false` means a normal bill, while `isClaimable=true` means a reimbursable claim.
|
|
6
6
|
|
|
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
|
+
|
|
7
9
|
**Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
|
|
8
10
|
|
|
9
11
|
## Requirements
|
|
@@ -41,7 +43,7 @@ The CLI talks to the production API: `https://budget-builder.getout.events`.
|
|
|
41
43
|
bb help
|
|
42
44
|
bb list-budgets
|
|
43
45
|
bb get-budget <budget-id>
|
|
44
|
-
bb update-budget-status <budget-id> <status>
|
|
46
|
+
bb update-budget-status <budget-id> <status> [--markProjectWon true] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
45
47
|
bb list-bills
|
|
46
48
|
bb approve-bill <bill-id>
|
|
47
49
|
bb list-approvals
|
|
@@ -52,18 +54,20 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
52
54
|
|
|
53
55
|
**Authoritative command list:** run `bb help` — it includes every command, positional args, and flags. MCP exposes a subset of the same tRPC surface; the CLI additionally includes a few procedures mainly used by the web UI (e.g. `reorder-budget-items`, `update-budget-item-supplier`). You can also use MCP-style `snake_case` (e.g. `bb list_bills`); it is normalized to kebab-case.
|
|
54
56
|
|
|
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
|
+
|
|
55
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).
|
|
56
60
|
|
|
57
61
|
### Command overview
|
|
58
62
|
|
|
59
63
|
| Area | Commands (non-exhaustive) |
|
|
60
64
|
| --- | --- |
|
|
61
|
-
| **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`, `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`) |
|
|
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`) |
|
|
62
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` |
|
|
63
67
|
| **Approvals** | `list-approvals` / `get-pending-approvals`, `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email) |
|
|
64
|
-
| **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`, `--
|
|
68
|
+
| **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`) |
|
|
65
69
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
66
|
-
| **Suppliers & items** | `list-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), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details
|
|
70
|
+
| **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) |
|
|
67
71
|
| **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
|
68
72
|
| **Errors** | `get-recent-errors`, `get-error-metrics` |
|
|
69
73
|
| **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
|
package/dist/commands.js
CHANGED
|
@@ -48,13 +48,16 @@ export async function getBudget(id) {
|
|
|
48
48
|
]);
|
|
49
49
|
out({ budget, items });
|
|
50
50
|
}
|
|
51
|
-
export async function updateBudgetStatus(budgetId, status) {
|
|
52
|
-
if (status === "ESTIMATE_ACCEPTED") {
|
|
53
|
-
throw new Error("Use mark-budget-won <budgetId> <filePath> so proof upload is included before setting ESTIMATE_ACCEPTED.");
|
|
54
|
-
}
|
|
51
|
+
export async function updateBudgetStatus(budgetId, status, opts) {
|
|
55
52
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
56
53
|
id: budgetId,
|
|
57
54
|
status,
|
|
55
|
+
...(opts?.markProjectWon !== undefined && {
|
|
56
|
+
markProjectWon: opts.markProjectWon,
|
|
57
|
+
}),
|
|
58
|
+
...(opts?.projectStatusOnCommercialRejection !== undefined && {
|
|
59
|
+
projectStatusOnCommercialRejection: opts.projectStatusOnCommercialRejection,
|
|
60
|
+
}),
|
|
58
61
|
});
|
|
59
62
|
out(result);
|
|
60
63
|
}
|
|
@@ -110,7 +113,7 @@ const runBudgetPreflight = async (budgetId, preflight) => {
|
|
|
110
113
|
insideSalesId: project.insideSalesId ?? "",
|
|
111
114
|
businessDevelopmentId: project.businessDevelopmentId ?? "",
|
|
112
115
|
venue: project.venue ?? "",
|
|
113
|
-
pax: String(project.pax),
|
|
116
|
+
pax: String(project.pax ?? ""),
|
|
114
117
|
...(project.slackChannelId &&
|
|
115
118
|
project.slackChannelUrl &&
|
|
116
119
|
project.slackChannelName
|
|
@@ -217,8 +220,14 @@ export async function listApprovals(opts) {
|
|
|
217
220
|
export async function listSuppliers(opts) {
|
|
218
221
|
const result = await api.supplier.getSuppliers.query({
|
|
219
222
|
page: opts.page ?? 1,
|
|
220
|
-
perPage: opts.perPage ??
|
|
223
|
+
perPage: opts.perPage ?? 10,
|
|
224
|
+
sort: opts.sort ?? [{ id: "createdAt", desc: true }],
|
|
221
225
|
name: opts.name,
|
|
226
|
+
createdBy: opts.createdBy,
|
|
227
|
+
gstRegistered: opts.gstRegistered ?? null,
|
|
228
|
+
status: opts.status,
|
|
229
|
+
supplierTags: opts.supplierTags,
|
|
230
|
+
active: opts.active ?? true,
|
|
222
231
|
});
|
|
223
232
|
out(result);
|
|
224
233
|
}
|
|
@@ -338,7 +347,7 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
|
338
347
|
});
|
|
339
348
|
out(confirmed);
|
|
340
349
|
}
|
|
341
|
-
export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
|
|
350
|
+
export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
342
351
|
const buf = await readFile(filePath);
|
|
343
352
|
const fileName = basename(filePath);
|
|
344
353
|
const size = buf.byteLength;
|
|
@@ -364,11 +373,14 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
|
|
|
364
373
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
365
374
|
id: budgetId,
|
|
366
375
|
status: "ESTIMATE_ACCEPTED",
|
|
376
|
+
...(opts?.markProjectWon !== undefined && {
|
|
377
|
+
markProjectWon: opts.markProjectWon,
|
|
378
|
+
}),
|
|
367
379
|
});
|
|
368
380
|
out(result);
|
|
369
381
|
}
|
|
370
|
-
export async function markBudgetWonWithProof(budgetId, filePath) {
|
|
371
|
-
await uploadBudgetWinProofFromPath(budgetId, filePath);
|
|
382
|
+
export async function markBudgetWonWithProof(budgetId, filePath, opts) {
|
|
383
|
+
await uploadBudgetWinProofFromPath(budgetId, filePath, opts);
|
|
372
384
|
}
|
|
373
385
|
export async function getBillDetails(id) {
|
|
374
386
|
const result = await api.bill.getById.query({ id });
|
|
@@ -508,6 +520,10 @@ export async function deleteSuppliersByIds(ids) {
|
|
|
508
520
|
const result = await api.supplier.deleteSuppliers.mutate({ ids });
|
|
509
521
|
out(result);
|
|
510
522
|
}
|
|
523
|
+
export async function reactivateSuppliersByIds(ids) {
|
|
524
|
+
const result = await api.supplier.reactivateSuppliers.mutate({ ids });
|
|
525
|
+
out(result);
|
|
526
|
+
}
|
|
511
527
|
export async function createSupplierCertification(name) {
|
|
512
528
|
const result = await api.supplier.createCertification.mutate({ name });
|
|
513
529
|
out(result);
|
|
@@ -742,7 +758,62 @@ export async function getSupplierDetails(supplierId) {
|
|
|
742
758
|
const supplier = await api.supplier.getSupplierById.query({
|
|
743
759
|
id: supplierId,
|
|
744
760
|
});
|
|
745
|
-
|
|
761
|
+
const respondedStatuses = new Set([
|
|
762
|
+
ExtendedApprovalStatus.APPROVED,
|
|
763
|
+
ExtendedApprovalStatus.REJECTED,
|
|
764
|
+
]);
|
|
765
|
+
const latestRespondedApproval = [
|
|
766
|
+
...supplier.approvals.filter((approval) => respondedStatuses.has(approval.status)),
|
|
767
|
+
].sort((a, b) => new Date(b.respondedAt ?? b.updatedAt).getTime() -
|
|
768
|
+
new Date(a.respondedAt ?? a.updatedAt).getTime() ||
|
|
769
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
770
|
+
const pendingApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.PENDING_APPROVAL);
|
|
771
|
+
const supersededApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.SUPERSEDED);
|
|
772
|
+
const respondedByApproval = latestRespondedApproval ?? null;
|
|
773
|
+
const responseUser = respondedByApproval
|
|
774
|
+
? (respondedByApproval.responder ?? respondedByApproval.approver)
|
|
775
|
+
: null;
|
|
776
|
+
out({
|
|
777
|
+
...supplier,
|
|
778
|
+
supplierApprovalSummary: {
|
|
779
|
+
pendingApprovalCount: pendingApprovals.length,
|
|
780
|
+
pendingApprovers: pendingApprovals.map((approval) => ({
|
|
781
|
+
approvalId: approval.id,
|
|
782
|
+
approverId: approval.approverId,
|
|
783
|
+
name: approval.approver.name,
|
|
784
|
+
email: approval.approver.email,
|
|
785
|
+
createdAt: approval.createdAt,
|
|
786
|
+
updatedAt: approval.updatedAt,
|
|
787
|
+
})),
|
|
788
|
+
supersededApprovalCount: supersededApprovals.length,
|
|
789
|
+
supersededApprovers: supersededApprovals.map((approval) => ({
|
|
790
|
+
approvalId: approval.id,
|
|
791
|
+
approverId: approval.approverId,
|
|
792
|
+
name: approval.approver.name,
|
|
793
|
+
email: approval.approver.email,
|
|
794
|
+
respondedAt: approval.respondedAt,
|
|
795
|
+
responder: approval.responder
|
|
796
|
+
? {
|
|
797
|
+
id: approval.responder.id,
|
|
798
|
+
name: approval.responder.name,
|
|
799
|
+
email: approval.responder.email,
|
|
800
|
+
}
|
|
801
|
+
: null,
|
|
802
|
+
})),
|
|
803
|
+
respondedBy: respondedByApproval && responseUser
|
|
804
|
+
? {
|
|
805
|
+
approvalId: respondedByApproval.id,
|
|
806
|
+
status: respondedByApproval.status,
|
|
807
|
+
approverId: respondedByApproval.approverId,
|
|
808
|
+
responderId: respondedByApproval.responderId,
|
|
809
|
+
name: responseUser.name,
|
|
810
|
+
email: responseUser.email,
|
|
811
|
+
respondedAt: respondedByApproval.respondedAt,
|
|
812
|
+
updatedAt: respondedByApproval.updatedAt,
|
|
813
|
+
}
|
|
814
|
+
: null,
|
|
815
|
+
},
|
|
816
|
+
});
|
|
746
817
|
}
|
|
747
818
|
export async function approveSupplier(supplierId) {
|
|
748
819
|
const notifications = await api.budget.getNotifications.query();
|
|
@@ -847,6 +918,7 @@ export async function listCompanies(opts) {
|
|
|
847
918
|
const result = await api.company.getCompanies.query({
|
|
848
919
|
name: opts.name,
|
|
849
920
|
createdBy: opts.createdBy,
|
|
921
|
+
active: opts.active,
|
|
850
922
|
page: opts.page ?? 1,
|
|
851
923
|
perPage: opts.perPage ?? 20,
|
|
852
924
|
});
|
|
@@ -865,6 +937,7 @@ export async function listProjects(opts) {
|
|
|
865
937
|
companyId: opts.companyId,
|
|
866
938
|
name: opts.name,
|
|
867
939
|
status: opts.status,
|
|
940
|
+
active: opts.active,
|
|
868
941
|
page: opts.page ?? 1,
|
|
869
942
|
perPage: opts.perPage ?? 20,
|
|
870
943
|
});
|
|
@@ -910,6 +983,9 @@ export async function updateProjectStatus(id, status, opts) {
|
|
|
910
983
|
...(opts?.projectManagerName && {
|
|
911
984
|
projectManagerName: opts.projectManagerName,
|
|
912
985
|
}),
|
|
986
|
+
...(opts?.wonOverrideReason && {
|
|
987
|
+
wonOverrideReason: opts.wonOverrideReason,
|
|
988
|
+
}),
|
|
913
989
|
});
|
|
914
990
|
out(result);
|
|
915
991
|
}
|
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, 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, 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, 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";
|
|
6
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
|
-
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
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";
|
|
9
9
|
function parsePositiveIntFlag(value, flagName) {
|
|
10
10
|
if (value === undefined)
|
|
@@ -26,6 +26,19 @@ function parseOptionalBoolFlag(flags, key) {
|
|
|
26
26
|
return false;
|
|
27
27
|
throw new Error(`--${key} must be true or false.`);
|
|
28
28
|
}
|
|
29
|
+
function parseCommercialRejectionProjectStatus(flags) {
|
|
30
|
+
const raw = flags.projectStatusOnCommercialRejection;
|
|
31
|
+
if (raw === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (raw === true) {
|
|
34
|
+
throw new Error("--projectStatusOnCommercialRejection requires PITCH or LOST.");
|
|
35
|
+
}
|
|
36
|
+
const status = String(raw).toUpperCase();
|
|
37
|
+
if (status === "PITCH" || status === "LOST") {
|
|
38
|
+
return status;
|
|
39
|
+
}
|
|
40
|
+
throw new Error("--projectStatusOnCommercialRejection must be PITCH or LOST.");
|
|
41
|
+
}
|
|
29
42
|
function parseListBillsFlags(flags) {
|
|
30
43
|
const statusRaw = getFlag(flags, "status");
|
|
31
44
|
const createdByIdsRaw = getFlag(flags, "createdByIds");
|
|
@@ -45,6 +58,45 @@ function parseListBillsFlags(flags) {
|
|
|
45
58
|
pageSize: parsePositiveIntFlag(getFlag(flags, "pageSize"), "--pageSize"),
|
|
46
59
|
};
|
|
47
60
|
}
|
|
61
|
+
const SUPPLIER_SORT_FIELDS = [
|
|
62
|
+
"createdAt",
|
|
63
|
+
"updatedAt",
|
|
64
|
+
"name",
|
|
65
|
+
"email",
|
|
66
|
+
"phone",
|
|
67
|
+
"gstRegistered",
|
|
68
|
+
"status",
|
|
69
|
+
"active",
|
|
70
|
+
];
|
|
71
|
+
function parseOptionalSupplierSortBy(raw) {
|
|
72
|
+
if (raw === undefined || raw === "")
|
|
73
|
+
return undefined;
|
|
74
|
+
if (!SUPPLIER_SORT_FIELDS.includes(raw)) {
|
|
75
|
+
throw new Error(`Invalid --sortBy "${raw}". Use one of: ${SUPPLIER_SORT_FIELDS.join(", ")}.`);
|
|
76
|
+
}
|
|
77
|
+
return raw;
|
|
78
|
+
}
|
|
79
|
+
function parseListSuppliersFlags(flags) {
|
|
80
|
+
const createdByRaw = getFlag(flags, "createdBy");
|
|
81
|
+
const statusRaw = getFlag(flags, "status");
|
|
82
|
+
const supplierTagsRaw = getFlag(flags, "supplierTags");
|
|
83
|
+
const sortBy = parseOptionalSupplierSortBy(getFlag(flags, "sortBy"));
|
|
84
|
+
const sortDirRaw = getFlag(flags, "sortDir");
|
|
85
|
+
const sortDir = parseOptionalBillListSortDir(sortDirRaw !== undefined ? String(sortDirRaw) : undefined);
|
|
86
|
+
return {
|
|
87
|
+
name: getFlag(flags, "name"),
|
|
88
|
+
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
|
|
89
|
+
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
|
|
90
|
+
sort: sortBy !== undefined || sortDir !== undefined
|
|
91
|
+
? [{ id: sortBy ?? "createdAt", desc: (sortDir ?? "desc") === "desc" }]
|
|
92
|
+
: undefined,
|
|
93
|
+
createdBy: parseCommaSeparatedIds(createdByRaw !== undefined ? String(createdByRaw) : undefined),
|
|
94
|
+
gstRegistered: parseOptionalBoolFlag(flags, "gstRegistered"),
|
|
95
|
+
status: parseCommaSeparatedSupplierStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
|
|
96
|
+
supplierTags: parseCommaSeparatedIds(supplierTagsRaw !== undefined ? String(supplierTagsRaw) : undefined),
|
|
97
|
+
active: parseOptionalBoolFlag(flags, "active"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
48
100
|
function parseDashboardQueryFlags(flags) {
|
|
49
101
|
const roleRaw = getFlag(flags, "role");
|
|
50
102
|
const timeFrameRaw = getFlag(flags, "timeFrame");
|
|
@@ -74,9 +126,11 @@ Budgets
|
|
|
74
126
|
get-budget-details <id> Full detail (margins, suppliers, etc.)
|
|
75
127
|
get-budget-categories
|
|
76
128
|
get-budget-versions <budgetId>
|
|
77
|
-
update-budget-status <id> <status>
|
|
129
|
+
update-budget-status <id> <status> [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
130
|
+
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, and auto-creates the Asana project/section when enabled.
|
|
131
|
+
ESTIMATE_REJECTED requires --projectStatusOnCommercialRejection when rejecting the only accepted/closed budget on a commercial project.
|
|
78
132
|
status: ${budgetStatusesForHelp.join(", ")}
|
|
79
|
-
mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
|
|
133
|
+
mark-budget-won <budgetId> <filePath> [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; PITCH or LOST projects become WON automatically
|
|
80
134
|
create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
|
|
81
135
|
update-budget --payload '<json>' (budget.updateBudget; must include id)
|
|
82
136
|
delete-budget <budgetId>
|
|
@@ -121,18 +175,21 @@ Approvals
|
|
|
121
175
|
reject-supplier <supplierId> --reason <text> (also sends requester reply email)
|
|
122
176
|
|
|
123
177
|
Companies & projects
|
|
124
|
-
list-companies [--name] [--createdBy] [--page] [--perPage]
|
|
178
|
+
list-companies [--name] [--createdBy] [--active true|false] [--page] [--perPage]
|
|
179
|
+
Omit --active to include both active and inactive companies.
|
|
125
180
|
get-company <id>
|
|
126
181
|
create-company --name --website <url>
|
|
127
182
|
update-company --payload '<json>' (company.updateCompany; must include id, name, website)
|
|
128
183
|
delete-company <id>
|
|
129
|
-
list-projects [--companyId] [--name] [--status] [--page] [--perPage]
|
|
184
|
+
list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
|
|
185
|
+
Omit --active to include both active and inactive projects.
|
|
130
186
|
get-project <id>
|
|
131
|
-
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --
|
|
187
|
+
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--pax] [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
|
|
132
188
|
update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
|
|
133
189
|
delete-project <id>
|
|
134
|
-
update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>]
|
|
190
|
+
update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
|
|
135
191
|
Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
|
|
192
|
+
Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
|
|
136
193
|
status: ${projectStatusesForHelp.join(", ")}
|
|
137
194
|
|
|
138
195
|
Contacts
|
|
@@ -141,10 +198,12 @@ Contacts
|
|
|
141
198
|
update-contact-person --payload '<json>' (contactPerson.updateContactPerson; must include id)
|
|
142
199
|
|
|
143
200
|
Suppliers & items
|
|
144
|
-
list-suppliers [--name] [--page] [--perPage]
|
|
201
|
+
list-suppliers [--name] [--page] [--perPage] [--sortBy createdAt|updatedAt|name|email|phone|gstRegistered|status|active] [--sortDir asc|desc] [--createdBy <csv>] [--gstRegistered true|false] [--status CSV] [--supplierTags <csv>] [--active true|false]
|
|
202
|
+
Defaults: --active true, --perPage 10, --sortBy createdAt, --sortDir desc (same as the web list); pass --active false for archived suppliers.
|
|
145
203
|
create-supplier --payload '<json>' (supplier.createSupplier; auto-requests approval + emails when status is PENDING_APPROVAL)
|
|
146
204
|
update-supplier --payload '<json>' (supplier.updateSupplier; must include id; auto-requests approval + emails when status is PENDING_APPROVAL)
|
|
147
|
-
delete-suppliers --ids <csv> (supplier.deleteSuppliers; admin)
|
|
205
|
+
delete-suppliers --ids <csv> (supplier.deleteSuppliers; admin; archives/deactivates supplier items)
|
|
206
|
+
reactivate-suppliers --ids <csv> (supplier.reactivateSuppliers; admin; reactivates supplier items)
|
|
148
207
|
create-certification --name (supplier.createCertification)
|
|
149
208
|
create-payment-method --name (supplier.createPaymentMethod)
|
|
150
209
|
create-supplier-role --name (supplier.createSupplierRole)
|
|
@@ -257,7 +316,9 @@ async function main() {
|
|
|
257
316
|
if (!budgetId || !filePath) {
|
|
258
317
|
throw new Error("mark-budget-won requires <budgetId> <filePath>");
|
|
259
318
|
}
|
|
260
|
-
await markBudgetWonWithProof(budgetId, filePath
|
|
319
|
+
await markBudgetWonWithProof(budgetId, filePath, {
|
|
320
|
+
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
321
|
+
});
|
|
261
322
|
break;
|
|
262
323
|
}
|
|
263
324
|
case "get-budget-items": {
|
|
@@ -293,7 +354,10 @@ async function main() {
|
|
|
293
354
|
if (!isBudgetStatusUpdate(status)) {
|
|
294
355
|
throw new Error(`Invalid status. Use one of: ${budgetStatusesForHelp.join(", ")}`);
|
|
295
356
|
}
|
|
296
|
-
await updateBudgetStatus(id, status
|
|
357
|
+
await updateBudgetStatus(id, status, {
|
|
358
|
+
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
359
|
+
projectStatusOnCommercialRejection: parseCommercialRejectionProjectStatus(flags),
|
|
360
|
+
});
|
|
297
361
|
break;
|
|
298
362
|
}
|
|
299
363
|
case "create-budget-approval": {
|
|
@@ -677,6 +741,7 @@ async function main() {
|
|
|
677
741
|
await listCompanies({
|
|
678
742
|
name: getFlag(flags, "name"),
|
|
679
743
|
createdBy: getFlag(flags, "createdBy"),
|
|
744
|
+
active: parseOptionalBoolFlag(flags, "active"),
|
|
680
745
|
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
|
|
681
746
|
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
|
|
682
747
|
});
|
|
@@ -720,6 +785,7 @@ async function main() {
|
|
|
720
785
|
companyId: getFlag(flags, "companyId"),
|
|
721
786
|
name: getFlag(flags, "name"),
|
|
722
787
|
status: parseOptionalExtendedProjectStatus(statusRaw !== undefined ? String(statusRaw) : undefined),
|
|
788
|
+
active: parseOptionalBoolFlag(flags, "active"),
|
|
723
789
|
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
|
|
724
790
|
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
|
|
725
791
|
});
|
|
@@ -751,13 +817,12 @@ async function main() {
|
|
|
751
817
|
!insideSalesId ||
|
|
752
818
|
!businessDevelopmentId ||
|
|
753
819
|
!venue ||
|
|
754
|
-
!pax ||
|
|
755
820
|
!asanaTaskId ||
|
|
756
821
|
!slackChannelId ||
|
|
757
822
|
!slackChannelUrl ||
|
|
758
823
|
!slackChannelName ||
|
|
759
824
|
!startDate) {
|
|
760
|
-
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --
|
|
825
|
+
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO/datetime flags as appropriate); optional --pax and --endDate");
|
|
761
826
|
}
|
|
762
827
|
await createProject({
|
|
763
828
|
name,
|
|
@@ -801,9 +866,13 @@ async function main() {
|
|
|
801
866
|
}
|
|
802
867
|
const pmIdFlag = getFlag(flags, "projectManagerId");
|
|
803
868
|
const pmNameFlag = getFlag(flags, "projectManagerName");
|
|
869
|
+
const wonOverrideReasonFlag = getFlag(flags, "wonOverrideReason");
|
|
804
870
|
await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
|
|
805
871
|
...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
|
|
806
872
|
...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
|
|
873
|
+
...(wonOverrideReasonFlag && {
|
|
874
|
+
wonOverrideReason: String(wonOverrideReasonFlag),
|
|
875
|
+
}),
|
|
807
876
|
});
|
|
808
877
|
break;
|
|
809
878
|
}
|
|
@@ -833,11 +902,7 @@ async function main() {
|
|
|
833
902
|
break;
|
|
834
903
|
}
|
|
835
904
|
case "list-suppliers": {
|
|
836
|
-
await listSuppliers(
|
|
837
|
-
name: getFlag(flags, "name"),
|
|
838
|
-
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
|
|
839
|
-
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
|
|
840
|
-
});
|
|
905
|
+
await listSuppliers(parseListSuppliersFlags(flags));
|
|
841
906
|
break;
|
|
842
907
|
}
|
|
843
908
|
case "create-supplier": {
|
|
@@ -870,6 +935,18 @@ async function main() {
|
|
|
870
935
|
await deleteSuppliersByIds(ids);
|
|
871
936
|
break;
|
|
872
937
|
}
|
|
938
|
+
case "reactivate-suppliers": {
|
|
939
|
+
const idsRaw = getFlag(flags, "ids");
|
|
940
|
+
if (idsRaw === undefined) {
|
|
941
|
+
throw new Error("reactivate-suppliers requires --ids <csv>");
|
|
942
|
+
}
|
|
943
|
+
const ids = parseCommaSeparatedIds(String(idsRaw));
|
|
944
|
+
if (!ids?.length) {
|
|
945
|
+
throw new Error("reactivate-suppliers requires non-empty --ids");
|
|
946
|
+
}
|
|
947
|
+
await reactivateSuppliersByIds(ids);
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
873
950
|
case "create-certification": {
|
|
874
951
|
const name = getFlag(flags, "name");
|
|
875
952
|
if (!name)
|
package/dist/parse-cli-enums.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BudgetRole, Deals, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
|
|
1
|
+
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
|
|
2
2
|
import { BillStatus, BudgetStatus, ProjectStatus } 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));
|
|
@@ -20,6 +20,7 @@ const SUPPLIER_ANALYTICS_TF = new Set([
|
|
|
20
20
|
"LAST_3_MONTHS",
|
|
21
21
|
"CUSTOM",
|
|
22
22
|
]);
|
|
23
|
+
const SUPPLIER_APPROVAL_STATUS_VALUES = new Set(Object.values(ExtendedApprovalStatus));
|
|
23
24
|
const EXT_ERROR_SEVERITY_VALUES = new Set(Object.values(ExtendedErrorSeverity));
|
|
24
25
|
const EXT_ERROR_STATUS_VALUES = new Set(Object.values(ExtendedErrorStatus));
|
|
25
26
|
export function isBudgetStatusListToken(s) {
|
|
@@ -144,6 +145,24 @@ export function parseCommaSeparatedBudgetStatuses(raw) {
|
|
|
144
145
|
}
|
|
145
146
|
return out;
|
|
146
147
|
}
|
|
148
|
+
export function parseCommaSeparatedSupplierStatuses(raw) {
|
|
149
|
+
if (raw === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
const parts = raw
|
|
152
|
+
.split(",")
|
|
153
|
+
.map((x) => x.trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
if (parts.length === 0)
|
|
156
|
+
return undefined;
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const p of parts) {
|
|
159
|
+
if (!SUPPLIER_APPROVAL_STATUS_VALUES.has(p)) {
|
|
160
|
+
throw new Error(`Invalid supplier approval status "${p}". Use one of: ${[...SUPPLIER_APPROVAL_STATUS_VALUES].join(", ")}.`);
|
|
161
|
+
}
|
|
162
|
+
out.push(p);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
147
166
|
export function parseBillStatusFlag(raw) {
|
|
148
167
|
if (raw === undefined)
|
|
149
168
|
return undefined;
|
|
@@ -214,7 +214,7 @@ export function parseUpdateProjectPayload(raw) {
|
|
|
214
214
|
insideSalesId: requiredString(o.insideSalesId, "update-project payload.insideSalesId"),
|
|
215
215
|
businessDevelopmentId: requiredString(o.businessDevelopmentId, "update-project payload.businessDevelopmentId"),
|
|
216
216
|
venue: requiredString(o.venue, "update-project payload.venue"),
|
|
217
|
-
pax:
|
|
217
|
+
pax: optionalString(o.pax),
|
|
218
218
|
dateRange: {
|
|
219
219
|
from: toDate(range.from, "dateRange.from"),
|
|
220
220
|
to: range.to === undefined || range.to === null
|
package/dist/prisma-enums.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|