@go-labs-sg/bb 1.6.1 → 1.7.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]
45
47
  bb list-bills
46
48
  bb approve-bill <bill-id>
47
49
  bb list-approvals
@@ -58,12 +60,12 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
58
60
 
59
61
  | Area | Commands (non-exhaustive) |
60
62
  | --- | --- |
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`) |
63
+ | **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` (`--markProjectWon true` can move the parent project to `WON` for accepted/closed estimates), `mark-budget-won` (`<budgetId>` + proof file path; optional `--markProjectWon true`), `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
64
  | **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
65
  | **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`) |
66
+ | **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
67
  | **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) |
68
+ | **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
69
  | **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
68
70
  | **Errors** | `get-recent-errors`, `get-error-metrics` |
69
71
  | **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,13 @@ 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
58
  });
59
59
  out(result);
60
60
  }
@@ -110,7 +110,7 @@ const runBudgetPreflight = async (budgetId, preflight) => {
110
110
  insideSalesId: project.insideSalesId ?? "",
111
111
  businessDevelopmentId: project.businessDevelopmentId ?? "",
112
112
  venue: project.venue ?? "",
113
- pax: String(project.pax),
113
+ pax: String(project.pax ?? ""),
114
114
  ...(project.slackChannelId &&
115
115
  project.slackChannelUrl &&
116
116
  project.slackChannelName
@@ -217,8 +217,14 @@ export async function listApprovals(opts) {
217
217
  export async function listSuppliers(opts) {
218
218
  const result = await api.supplier.getSuppliers.query({
219
219
  page: opts.page ?? 1,
220
- perPage: opts.perPage ?? 20,
220
+ perPage: opts.perPage ?? 10,
221
+ sort: opts.sort ?? [{ id: "createdAt", desc: true }],
221
222
  name: opts.name,
223
+ createdBy: opts.createdBy,
224
+ gstRegistered: opts.gstRegistered ?? null,
225
+ status: opts.status,
226
+ supplierTags: opts.supplierTags,
227
+ active: opts.active ?? true,
222
228
  });
223
229
  out(result);
224
230
  }
@@ -338,7 +344,7 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
338
344
  });
339
345
  out(confirmed);
340
346
  }
341
- export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
347
+ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
342
348
  const buf = await readFile(filePath);
343
349
  const fileName = basename(filePath);
344
350
  const size = buf.byteLength;
@@ -364,11 +370,14 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
364
370
  const result = await api.budget.updateBudgetStatus.mutate({
365
371
  id: budgetId,
366
372
  status: "ESTIMATE_ACCEPTED",
373
+ ...(opts?.markProjectWon !== undefined && {
374
+ markProjectWon: opts.markProjectWon,
375
+ }),
367
376
  });
368
377
  out(result);
369
378
  }
370
- export async function markBudgetWonWithProof(budgetId, filePath) {
371
- await uploadBudgetWinProofFromPath(budgetId, filePath);
379
+ export async function markBudgetWonWithProof(budgetId, filePath, opts) {
380
+ await uploadBudgetWinProofFromPath(budgetId, filePath, opts);
372
381
  }
373
382
  export async function getBillDetails(id) {
374
383
  const result = await api.bill.getById.query({ id });
@@ -508,6 +517,10 @@ export async function deleteSuppliersByIds(ids) {
508
517
  const result = await api.supplier.deleteSuppliers.mutate({ ids });
509
518
  out(result);
510
519
  }
520
+ export async function reactivateSuppliersByIds(ids) {
521
+ const result = await api.supplier.reactivateSuppliers.mutate({ ids });
522
+ out(result);
523
+ }
511
524
  export async function createSupplierCertification(name) {
512
525
  const result = await api.supplier.createCertification.mutate({ name });
513
526
  out(result);
@@ -742,7 +755,62 @@ export async function getSupplierDetails(supplierId) {
742
755
  const supplier = await api.supplier.getSupplierById.query({
743
756
  id: supplierId,
744
757
  });
745
- out(supplier);
758
+ const respondedStatuses = new Set([
759
+ ExtendedApprovalStatus.APPROVED,
760
+ ExtendedApprovalStatus.REJECTED,
761
+ ]);
762
+ const latestRespondedApproval = [
763
+ ...supplier.approvals.filter((approval) => respondedStatuses.has(approval.status)),
764
+ ].sort((a, b) => new Date(b.respondedAt ?? b.updatedAt).getTime() -
765
+ new Date(a.respondedAt ?? a.updatedAt).getTime() ||
766
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
767
+ const pendingApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.PENDING_APPROVAL);
768
+ const supersededApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.SUPERSEDED);
769
+ const respondedByApproval = latestRespondedApproval ?? null;
770
+ const responseUser = respondedByApproval
771
+ ? (respondedByApproval.responder ?? respondedByApproval.approver)
772
+ : null;
773
+ out({
774
+ ...supplier,
775
+ supplierApprovalSummary: {
776
+ pendingApprovalCount: pendingApprovals.length,
777
+ pendingApprovers: pendingApprovals.map((approval) => ({
778
+ approvalId: approval.id,
779
+ approverId: approval.approverId,
780
+ name: approval.approver.name,
781
+ email: approval.approver.email,
782
+ createdAt: approval.createdAt,
783
+ updatedAt: approval.updatedAt,
784
+ })),
785
+ supersededApprovalCount: supersededApprovals.length,
786
+ supersededApprovers: supersededApprovals.map((approval) => ({
787
+ approvalId: approval.id,
788
+ approverId: approval.approverId,
789
+ name: approval.approver.name,
790
+ email: approval.approver.email,
791
+ respondedAt: approval.respondedAt,
792
+ responder: approval.responder
793
+ ? {
794
+ id: approval.responder.id,
795
+ name: approval.responder.name,
796
+ email: approval.responder.email,
797
+ }
798
+ : null,
799
+ })),
800
+ respondedBy: respondedByApproval && responseUser
801
+ ? {
802
+ approvalId: respondedByApproval.id,
803
+ status: respondedByApproval.status,
804
+ approverId: respondedByApproval.approverId,
805
+ responderId: respondedByApproval.responderId,
806
+ name: responseUser.name,
807
+ email: responseUser.email,
808
+ respondedAt: respondedByApproval.respondedAt,
809
+ updatedAt: respondedByApproval.updatedAt,
810
+ }
811
+ : null,
812
+ },
813
+ });
746
814
  }
747
815
  export async function approveSupplier(supplierId) {
748
816
  const notifications = await api.budget.getNotifications.query();
@@ -910,6 +978,9 @@ export async function updateProjectStatus(id, status, opts) {
910
978
  ...(opts?.projectManagerName && {
911
979
  projectManagerName: opts.projectManagerName,
912
980
  }),
981
+ ...(opts?.wonOverrideReason && {
982
+ wonOverrideReason: opts.wonOverrideReason,
983
+ }),
913
984
  });
914
985
  out(result);
915
986
  }
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)
@@ -45,6 +45,45 @@ function parseListBillsFlags(flags) {
45
45
  pageSize: parsePositiveIntFlag(getFlag(flags, "pageSize"), "--pageSize"),
46
46
  };
47
47
  }
48
+ const SUPPLIER_SORT_FIELDS = [
49
+ "createdAt",
50
+ "updatedAt",
51
+ "name",
52
+ "email",
53
+ "phone",
54
+ "gstRegistered",
55
+ "status",
56
+ "active",
57
+ ];
58
+ function parseOptionalSupplierSortBy(raw) {
59
+ if (raw === undefined || raw === "")
60
+ return undefined;
61
+ if (!SUPPLIER_SORT_FIELDS.includes(raw)) {
62
+ throw new Error(`Invalid --sortBy "${raw}". Use one of: ${SUPPLIER_SORT_FIELDS.join(", ")}.`);
63
+ }
64
+ return raw;
65
+ }
66
+ function parseListSuppliersFlags(flags) {
67
+ const createdByRaw = getFlag(flags, "createdBy");
68
+ const statusRaw = getFlag(flags, "status");
69
+ const supplierTagsRaw = getFlag(flags, "supplierTags");
70
+ const sortBy = parseOptionalSupplierSortBy(getFlag(flags, "sortBy"));
71
+ const sortDirRaw = getFlag(flags, "sortDir");
72
+ const sortDir = parseOptionalBillListSortDir(sortDirRaw !== undefined ? String(sortDirRaw) : undefined);
73
+ return {
74
+ name: getFlag(flags, "name"),
75
+ page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
76
+ perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
77
+ sort: sortBy !== undefined || sortDir !== undefined
78
+ ? [{ id: sortBy ?? "createdAt", desc: (sortDir ?? "desc") === "desc" }]
79
+ : undefined,
80
+ createdBy: parseCommaSeparatedIds(createdByRaw !== undefined ? String(createdByRaw) : undefined),
81
+ gstRegistered: parseOptionalBoolFlag(flags, "gstRegistered"),
82
+ status: parseCommaSeparatedSupplierStatuses(statusRaw !== undefined ? String(statusRaw) : undefined),
83
+ supplierTags: parseCommaSeparatedIds(supplierTagsRaw !== undefined ? String(supplierTagsRaw) : undefined),
84
+ active: parseOptionalBoolFlag(flags, "active"),
85
+ };
86
+ }
48
87
  function parseDashboardQueryFlags(flags) {
49
88
  const roleRaw = getFlag(flags, "role");
50
89
  const timeFrameRaw = getFlag(flags, "timeFrame");
@@ -74,9 +113,9 @@ Budgets
74
113
  get-budget-details <id> Full detail (margins, suppliers, etc.)
75
114
  get-budget-categories
76
115
  get-budget-versions <budgetId>
77
- update-budget-status <id> <status>
116
+ update-budget-status <id> <status> [--markProjectWon true|false]
78
117
  status: ${budgetStatusesForHelp.join(", ")}
79
- mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
118
+ mark-budget-won <budgetId> <filePath> [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
80
119
  create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
81
120
  update-budget --payload '<json>' (budget.updateBudget; must include id)
82
121
  delete-budget <budgetId>
@@ -128,11 +167,12 @@ Companies & projects
128
167
  delete-company <id>
129
168
  list-projects [--companyId] [--name] [--status] [--page] [--perPage]
130
169
  get-project <id>
131
- create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
170
+ create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--pax] [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
132
171
  update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
133
172
  delete-project <id>
134
- update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>]
173
+ update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
135
174
  Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
175
+ Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
136
176
  status: ${projectStatusesForHelp.join(", ")}
137
177
 
138
178
  Contacts
@@ -141,10 +181,12 @@ Contacts
141
181
  update-contact-person --payload '<json>' (contactPerson.updateContactPerson; must include id)
142
182
 
143
183
  Suppliers & items
144
- list-suppliers [--name] [--page] [--perPage]
184
+ 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]
185
+ Defaults: --active true, --perPage 10, --sortBy createdAt, --sortDir desc (same as the web list); pass --active false for archived suppliers.
145
186
  create-supplier --payload '<json>' (supplier.createSupplier; auto-requests approval + emails when status is PENDING_APPROVAL)
146
187
  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)
188
+ delete-suppliers --ids <csv> (supplier.deleteSuppliers; admin; archives/deactivates supplier items)
189
+ reactivate-suppliers --ids <csv> (supplier.reactivateSuppliers; admin; reactivates supplier items)
148
190
  create-certification --name (supplier.createCertification)
149
191
  create-payment-method --name (supplier.createPaymentMethod)
150
192
  create-supplier-role --name (supplier.createSupplierRole)
@@ -257,7 +299,9 @@ async function main() {
257
299
  if (!budgetId || !filePath) {
258
300
  throw new Error("mark-budget-won requires <budgetId> <filePath>");
259
301
  }
260
- await markBudgetWonWithProof(budgetId, filePath);
302
+ await markBudgetWonWithProof(budgetId, filePath, {
303
+ markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
304
+ });
261
305
  break;
262
306
  }
263
307
  case "get-budget-items": {
@@ -293,7 +337,9 @@ async function main() {
293
337
  if (!isBudgetStatusUpdate(status)) {
294
338
  throw new Error(`Invalid status. Use one of: ${budgetStatusesForHelp.join(", ")}`);
295
339
  }
296
- await updateBudgetStatus(id, status);
340
+ await updateBudgetStatus(id, status, {
341
+ markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
342
+ });
297
343
  break;
298
344
  }
299
345
  case "create-budget-approval": {
@@ -751,13 +797,12 @@ async function main() {
751
797
  !insideSalesId ||
752
798
  !businessDevelopmentId ||
753
799
  !venue ||
754
- !pax ||
755
800
  !asanaTaskId ||
756
801
  !slackChannelId ||
757
802
  !slackChannelUrl ||
758
803
  !slackChannelName ||
759
804
  !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");
805
+ 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
806
  }
762
807
  await createProject({
763
808
  name,
@@ -801,9 +846,13 @@ async function main() {
801
846
  }
802
847
  const pmIdFlag = getFlag(flags, "projectManagerId");
803
848
  const pmNameFlag = getFlag(flags, "projectManagerName");
849
+ const wonOverrideReasonFlag = getFlag(flags, "wonOverrideReason");
804
850
  await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
805
851
  ...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
806
852
  ...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
853
+ ...(wonOverrideReasonFlag && {
854
+ wonOverrideReason: String(wonOverrideReasonFlag),
855
+ }),
807
856
  });
808
857
  break;
809
858
  }
@@ -833,11 +882,7 @@ async function main() {
833
882
  break;
834
883
  }
835
884
  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
- });
885
+ await listSuppliers(parseListSuppliersFlags(flags));
841
886
  break;
842
887
  }
843
888
  case "create-supplier": {
@@ -870,6 +915,18 @@ async function main() {
870
915
  await deleteSuppliersByIds(ids);
871
916
  break;
872
917
  }
918
+ case "reactivate-suppliers": {
919
+ const idsRaw = getFlag(flags, "ids");
920
+ if (idsRaw === undefined) {
921
+ throw new Error("reactivate-suppliers requires --ids <csv>");
922
+ }
923
+ const ids = parseCommaSeparatedIds(String(idsRaw));
924
+ if (!ids?.length) {
925
+ throw new Error("reactivate-suppliers requires non-empty --ids");
926
+ }
927
+ await reactivateSuppliersByIds(ids);
928
+ break;
929
+ }
873
930
  case "create-certification": {
874
931
  const name = getFlag(flags, "name");
875
932
  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.7.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",