@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 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`, `--pax`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; 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`) |
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`, `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) |
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 ?? 20,
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
- out(supplier);
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 --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
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 --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO/datetime flags as appropriate); optional --endDate");
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)
@@ -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: requiredString(o.pax, "update-project payload.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
@@ -27,6 +27,7 @@ export const ApprovalStatus = {
27
27
  PENDING_APPROVAL: "PENDING_APPROVAL",
28
28
  APPROVED: "APPROVED",
29
29
  REJECTED: "REJECTED",
30
+ SUPERSEDED: "SUPERSEDED",
30
31
  };
31
32
  export const ApprovalType = {
32
33
  BUDGET: "BUDGET",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.6.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",