@go-labs-sg/bb 1.17.0 → 1.18.1

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
@@ -70,10 +70,12 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
70
70
 
71
71
  **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.
72
72
 
73
- **Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. If the parent project is `PITCH` or `LOST`, the API now marks it `WON` automatically and, when the `asana-tasks` feature flag is enabled, creates the Asana project/section/tasks automatically. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
73
+ **Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. If the parent project is `PITCH` or `LOST`, the API now marks it `WON` automatically and, when the `asana-tasks` feature flag is enabled, creates an Asana Event Ops section from Prompt 5 tasks. Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
74
74
 
75
75
  **Sensitive workflow changes:** Approval decisions/requests, entity status updates, `mark-budget-won`, `submit-quotation`, and supplier approval request side effects require an interactive `CONFIRM` prompt. Non-interactive runs abort before the guarded workflow mutation. Agents must get user confirmation in chat before attempting the command; the prompt is a final runtime guard, not a replacement for user approval.
76
76
 
77
+ **Approval email exclusions:** Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
78
+
77
79
  **Locked estimate budget changes:** Budget-changing CLI commands require an interactive `yes` confirmation when the current budget status is `ESTIMATE_CREATED`, `ESTIMATE_SENT`, `ESTIMATE_ACCEPTED`, `ESTIMATE_REJECTED`, or `ESTIMATE_CLOSED`. There is no force/override flag; non-interactive runs abort before mutating locked budgets.
78
80
 
79
81
  **Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. Plain `description` fields for item create/update are converted to `descriptionRichText`; pass `descriptionRichText` directly when formatted Tiptap JSON is required. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
@@ -82,11 +84,11 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
82
84
 
83
85
  | Area | Commands (non-exhaustive) |
84
86
  | --- | --- |
85
- | **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`) |
86
- | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
87
- | **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
87
+ | **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
88
+ | **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (moving a bill or claim to `PAID` sends creator and supplier emails, CC `m@getout.sg` and BCC `noah@getout.sg`; a claim sends one email when both recipient addresses match), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
89
+ | **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
88
90
  | **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation` |
89
- | **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`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`; always requests QBO project import like the web app), `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`) |
91
+ | **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project` (its budget overview returns `totalRevenue`, calculated only from Estimate Accepted and Estimate Closed budgets), `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`, `--projectManagerId`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `projectManagerId` and `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `RECONCILED` \| `LOST`; close-out follows `WON` → `RECONCILED` → `COMPLETED`, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; for `PITCH` → `WON` also pass a Budget Builder user `--projectManagerId` or `--projectManagerEmail`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
90
92
  | **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
91
93
  | **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) |
92
94
  | **Dashboard & users** | `list-users`, `create-user` (admin; provisions an API-only service identity with no Google sign-in; `--email`, optional `--name`, optional `--role` defaulting to `USER`), `create-api-key` (admin; `--userId`, `--name`; raw key shown once), `list-api-keys` (admin; optional `--userId`), `revoke-api-key` (admin; key ID plus `--userId` for another user's key), `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
package/dist/commands.js CHANGED
@@ -19,6 +19,34 @@ const ASANA_WON_LOST_SECTION_GIDS = new Set([
19
19
  ]);
20
20
  const MAX_ASANA_LEAD_CANDIDATES = 5;
21
21
  const ASANA_SEARCH_DETAIL_LIMIT = 15;
22
+ const NOTIFICATION_PAGE_LIMIT = 50;
23
+ const getAllNotifications = async () => {
24
+ const notifications = [];
25
+ let cursor;
26
+ do {
27
+ const page = await api.budget.getNotifications.query({
28
+ cursor,
29
+ limit: NOTIFICATION_PAGE_LIMIT,
30
+ });
31
+ notifications.push(...page.notifications);
32
+ cursor = page.nextCursor;
33
+ } while (cursor);
34
+ return notifications;
35
+ };
36
+ const createApprovalEmailDeliveryReport = ({ sentCount, skippedCount, message, }) => ({
37
+ sent: sentCount > 0,
38
+ count: sentCount,
39
+ sentCount,
40
+ skippedCount,
41
+ message,
42
+ });
43
+ const createApprovalEmailDeliveryFailure = (error) => ({
44
+ sent: false,
45
+ count: 0,
46
+ sentCount: 0,
47
+ skippedCount: 0,
48
+ error,
49
+ });
22
50
  function isPendingRequest(n) {
23
51
  return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
24
52
  }
@@ -212,6 +240,27 @@ const contentTypeHeaderForFileName = (fileName) => {
212
240
  const ct = contentType(fileName);
213
241
  return typeof ct === "string" ? ct : "application/octet-stream";
214
242
  };
243
+ const supportedAttachmentContentTypes = [
244
+ "application/pdf",
245
+ "image/gif",
246
+ "image/jpeg",
247
+ "image/png",
248
+ "image/webp",
249
+ ];
250
+ const attachmentContentTypeForFileName = (fileName) => {
251
+ const resolvedContentType = contentTypeHeaderForFileName(fileName);
252
+ const supportedContentType = supportedAttachmentContentTypes.find((contentTypeValue) => contentTypeValue === resolvedContentType);
253
+ if (supportedContentType)
254
+ return supportedContentType;
255
+ throw new Error(`Unsupported attachment type: ${fileName}`);
256
+ };
257
+ const billAttachmentContentTypeForFileName = (fileName) => {
258
+ const resolvedContentType = attachmentContentTypeForFileName(fileName);
259
+ if (resolvedContentType === "image/webp") {
260
+ throw new Error(`Unsupported QuickBooks bill attachment type: ${fileName}. Use PDF, GIF, JPEG, or PNG.`);
261
+ }
262
+ return resolvedContentType;
263
+ };
215
264
  const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
216
265
  BudgetStatus.ESTIMATE_CREATED,
217
266
  BudgetStatus.ESTIMATE_SENT,
@@ -424,7 +473,7 @@ export async function approveBill(billId) {
424
473
  entity: `bill ${billId}`,
425
474
  details: "and send requester reply email",
426
475
  });
427
- const notifications = await api.budget.getNotifications.query();
476
+ const notifications = await getAllNotifications();
428
477
  const pending = notifications.filter((n) => isPendingRequest(n) &&
429
478
  n.type === BILL &&
430
479
  (n.billId === billId || n.bill?.id === billId));
@@ -436,6 +485,10 @@ export async function approveBill(billId) {
436
485
  billApprovalId: approval.id,
437
486
  status: "APPROVED",
438
487
  });
488
+ if (result.processing) {
489
+ out(result);
490
+ return;
491
+ }
439
492
  let email;
440
493
  try {
441
494
  await api.email.sendReplyBillApprovalEmail.mutate(result.updatedApproval);
@@ -453,7 +506,7 @@ export async function approveBill(billId) {
453
506
  });
454
507
  }
455
508
  export async function listApprovals(opts) {
456
- const notifications = await api.budget.getNotifications.query();
509
+ const notifications = await getAllNotifications();
457
510
  const pending = notifications.filter(isPendingRequest);
458
511
  const typeFilter = opts.type === undefined || opts.type === "ALL"
459
512
  ? (_n) => true
@@ -524,19 +577,15 @@ export async function createBillApproval(billId) {
524
577
  const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
525
578
  let email;
526
579
  if (approvalIds.length === 0) {
527
- email = { sent: false, count: 0, error: "No approval ids returned" };
580
+ email = createApprovalEmailDeliveryFailure("No approval ids returned");
528
581
  }
529
582
  else {
530
583
  try {
531
- await api.email.sendBillApprovalRequestEmail.mutate(approvalIds);
532
- email = { sent: true, count: approvalIds.length };
584
+ const delivery = await api.email.sendBillApprovalRequestEmail.mutate(approvalIds);
585
+ email = createApprovalEmailDeliveryReport(delivery);
533
586
  }
534
587
  catch (error) {
535
- email = {
536
- sent: false,
537
- count: approvalIds.length,
538
- error: error instanceof Error ? error.message : String(error),
539
- };
588
+ email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
540
589
  }
541
590
  }
542
591
  out({
@@ -589,6 +638,36 @@ export async function getBillAttachments(id) {
589
638
  const result = await api.bill.getAttachments.query({ id });
590
639
  out(result);
591
640
  }
641
+ export async function stageBillAttachmentsFromPaths(projectId, filePaths) {
642
+ const attachments = [];
643
+ for (const filePath of filePaths) {
644
+ const buffer = await readFile(filePath);
645
+ const fileName = basename(filePath);
646
+ const size = buffer.byteLength;
647
+ const contentType = billAttachmentContentTypeForFileName(fileName);
648
+ const { uploadUrl, key } = await api.attachment.requestStagedBillAttachmentUpload.mutate({
649
+ projectId,
650
+ fileName,
651
+ size,
652
+ contentType,
653
+ });
654
+ const response = await fetch(uploadUrl, {
655
+ method: "PUT",
656
+ body: buffer,
657
+ headers: { "Content-Type": contentType },
658
+ });
659
+ if (!response.ok) {
660
+ throw new Error(`S3 upload failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
661
+ }
662
+ attachments.push({ id: randomUUID(), key, name: fileName, size });
663
+ }
664
+ out({
665
+ success: true,
666
+ projectId,
667
+ count: attachments.length,
668
+ attachments,
669
+ });
670
+ }
592
671
  export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
593
672
  const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
594
673
  out(confirmed);
@@ -597,15 +676,17 @@ const uploadSingleBillAttachmentFromPath = async (billId, filePath, type = "BILL
597
676
  const buf = await readFile(filePath);
598
677
  const fileName = basename(filePath);
599
678
  const size = buf.byteLength;
679
+ const attachmentContentType = billAttachmentContentTypeForFileName(fileName);
600
680
  const { uploadUrl, key } = await api.attachment.requestBillAttachmentUpload.mutate({
601
681
  billId,
682
+ contentType: attachmentContentType,
602
683
  fileName,
603
684
  size,
604
685
  });
605
686
  const res = await fetch(uploadUrl, {
606
687
  method: "PUT",
607
688
  body: buf,
608
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
689
+ headers: { "Content-Type": attachmentContentType },
609
690
  });
610
691
  if (!res.ok) {
611
692
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -688,15 +769,17 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
688
769
  const buf = await readFile(filePath);
689
770
  const fileName = basename(filePath);
690
771
  const size = buf.byteLength;
772
+ const attachmentContentType = attachmentContentTypeForFileName(fileName);
691
773
  const { uploadUrl, key } = await api.attachment.requestBudgetAttachmentUpload.mutate({
692
774
  budgetId,
775
+ contentType: attachmentContentType,
693
776
  fileName,
694
777
  size,
695
778
  });
696
779
  const res = await fetch(uploadUrl, {
697
780
  method: "PUT",
698
781
  body: buf,
699
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
782
+ headers: { "Content-Type": attachmentContentType },
700
783
  });
701
784
  if (!res.ok) {
702
785
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -719,15 +802,17 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
719
802
  const buf = await readFile(filePath);
720
803
  const fileName = basename(filePath);
721
804
  const size = buf.byteLength;
805
+ const attachmentContentType = attachmentContentTypeForFileName(fileName);
722
806
  const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
723
807
  budgetId,
808
+ contentType: attachmentContentType,
724
809
  fileName,
725
810
  size,
726
811
  });
727
812
  const res = await fetch(uploadUrl, {
728
813
  method: "PUT",
729
814
  body: buf,
730
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
815
+ headers: { "Content-Type": attachmentContentType },
731
816
  });
732
817
  if (!res.ok) {
733
818
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -816,19 +901,15 @@ export async function createBudgetApproval(budgetId, preflight) {
816
901
  const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
817
902
  let email;
818
903
  if (approvalIds.length === 0) {
819
- email = { sent: false, count: 0, error: "No approval ids returned" };
904
+ email = createApprovalEmailDeliveryFailure("No approval ids returned");
820
905
  }
821
906
  else {
822
907
  try {
823
- await api.email.sendApprovalRequestEmail.mutate(approvalIds);
824
- email = { sent: true, count: approvalIds.length };
908
+ const delivery = await api.email.sendApprovalRequestEmail.mutate(approvalIds);
909
+ email = createApprovalEmailDeliveryReport(delivery);
825
910
  }
826
911
  catch (error) {
827
- email = {
828
- sent: false,
829
- count: approvalIds.length,
830
- error: error instanceof Error ? error.message : String(error),
831
- };
912
+ email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
832
913
  }
833
914
  }
834
915
  out({
@@ -1009,19 +1090,15 @@ export async function createSupplierFromPayload(raw) {
1009
1090
  let emailResult;
1010
1091
  const approvalIds = (approval.results ?? []).map((item) => ({ id: item.id }));
1011
1092
  if (approvalIds.length === 0) {
1012
- emailResult = { sent: false, count: 0, error: "No approval ids returned" };
1093
+ emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
1013
1094
  }
1014
1095
  else {
1015
1096
  try {
1016
- await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1017
- emailResult = { sent: true, count: approvalIds.length };
1097
+ const delivery = await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1098
+ emailResult = createApprovalEmailDeliveryReport(delivery);
1018
1099
  }
1019
1100
  catch (error) {
1020
- emailResult = {
1021
- sent: false,
1022
- count: approvalIds.length,
1023
- error: error instanceof Error ? error.message : String(error),
1024
- };
1101
+ emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
1025
1102
  }
1026
1103
  }
1027
1104
  out({
@@ -1058,27 +1135,23 @@ export async function updateSupplierFromPayload(raw) {
1058
1135
  approvalResult = {
1059
1136
  requested: true,
1060
1137
  approvalCount: 0,
1061
- email: { sent: false, count: 0, error: "No approval ids returned" },
1138
+ email: createApprovalEmailDeliveryFailure("No approval ids returned"),
1062
1139
  };
1063
1140
  }
1064
1141
  else {
1065
1142
  try {
1066
- await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1143
+ const delivery = await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1067
1144
  approvalResult = {
1068
1145
  requested: true,
1069
1146
  approvalCount: approvalIds.length,
1070
- email: { sent: true, count: approvalIds.length },
1147
+ email: createApprovalEmailDeliveryReport(delivery),
1071
1148
  };
1072
1149
  }
1073
1150
  catch (error) {
1074
1151
  approvalResult = {
1075
1152
  requested: true,
1076
1153
  approvalCount: approvalIds.length,
1077
- email: {
1078
- sent: false,
1079
- count: approvalIds.length,
1080
- error: error instanceof Error ? error.message : String(error),
1081
- },
1154
+ email: createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error)),
1082
1155
  };
1083
1156
  }
1084
1157
  }
@@ -1131,7 +1204,7 @@ export async function approveBudget(budgetId) {
1131
1204
  entity: `budget ${budgetId}`,
1132
1205
  details: "and send requester reply email",
1133
1206
  });
1134
- const notifications = await api.budget.getNotifications.query();
1207
+ const notifications = await getAllNotifications();
1135
1208
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1136
1209
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1137
1210
  n.type === ExtendedApprovalType.BUDGET &&
@@ -1167,7 +1240,7 @@ export async function rejectBudget(budgetId, reason) {
1167
1240
  entity: `budget ${budgetId}`,
1168
1241
  details: `with reason "${reason}" and send requester reply email`,
1169
1242
  });
1170
- const notifications = await api.budget.getNotifications.query();
1243
+ const notifications = await getAllNotifications();
1171
1244
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1172
1245
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1173
1246
  n.type === ExtendedApprovalType.BUDGET &&
@@ -1265,7 +1338,7 @@ export async function approveSupplier(supplierId) {
1265
1338
  entity: `supplier ${supplierId}`,
1266
1339
  details: "and send requester reply email",
1267
1340
  });
1268
- const notifications = await api.budget.getNotifications.query();
1341
+ const notifications = await getAllNotifications();
1269
1342
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1270
1343
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1271
1344
  n.type === ExtendedApprovalType.SUPPLIER &&
@@ -1303,7 +1376,7 @@ export async function rejectSupplier(supplierId, reason) {
1303
1376
  entity: `supplier ${supplierId}`,
1304
1377
  details: `with reason "${reason}" and send requester reply email`,
1305
1378
  });
1306
- const notifications = await api.budget.getNotifications.query();
1379
+ const notifications = await getAllNotifications();
1307
1380
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1308
1381
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1309
1382
  n.type === ExtendedApprovalType.SUPPLIER &&
@@ -1342,7 +1415,7 @@ export async function rejectBill(billId, reason) {
1342
1415
  entity: `bill ${billId}`,
1343
1416
  details: `with reason "${reason}" and send requester reply email`,
1344
1417
  });
1345
- const notifications = await api.budget.getNotifications.query();
1418
+ const notifications = await getAllNotifications();
1346
1419
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1347
1420
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1348
1421
  n.type === ExtendedApprovalType.BILL &&
@@ -1356,6 +1429,10 @@ export async function rejectBill(billId, reason) {
1356
1429
  status: ExtendedApprovalStatus.REJECTED,
1357
1430
  rejectionReason: reason,
1358
1431
  });
1432
+ if (result.processing) {
1433
+ out(result);
1434
+ return;
1435
+ }
1359
1436
  let email;
1360
1437
  try {
1361
1438
  await api.email.sendReplyBillApprovalEmail.mutate(result.updatedApproval);
@@ -1377,7 +1454,7 @@ export async function approveQuotation(quotationId) {
1377
1454
  action: "Approve quotation",
1378
1455
  entity: `quotation ${quotationId}`,
1379
1456
  });
1380
- const notifications = await api.budget.getNotifications.query();
1457
+ const notifications = await getAllNotifications();
1381
1458
  const pending = notifications.filter((n) => isPendingRequest(n) &&
1382
1459
  n.type === QUOTATION &&
1383
1460
  (n.supplierQuotationId === quotationId ||
@@ -1397,7 +1474,7 @@ export async function rejectQuotation(quotationId, reason) {
1397
1474
  entity: `quotation ${quotationId}`,
1398
1475
  details: `with reason "${reason}"`,
1399
1476
  });
1400
- const notifications = await api.budget.getNotifications.query();
1477
+ const notifications = await getAllNotifications();
1401
1478
  const pending = notifications.filter((n) => isPendingRequest(n) &&
1402
1479
  n.type === QUOTATION &&
1403
1480
  (n.supplierQuotationId === quotationId ||
@@ -1466,6 +1543,7 @@ export async function createProject(opts) {
1466
1543
  contactPersonId: opts.contactPersonId,
1467
1544
  insideSalesId: opts.insideSalesId,
1468
1545
  businessDevelopmentId: opts.businessDevelopmentId,
1546
+ projectManagerId: opts.projectManagerId,
1469
1547
  venue: opts.venue,
1470
1548
  pax: opts.pax,
1471
1549
  asanaTaskId: links.asanaTaskId,
@@ -1489,8 +1567,8 @@ export async function updateProjectStatus(id, status, opts) {
1489
1567
  id,
1490
1568
  status,
1491
1569
  ...(opts?.projectManagerId && { projectManagerId: opts.projectManagerId }),
1492
- ...(opts?.projectManagerName && {
1493
- projectManagerName: opts.projectManagerName,
1570
+ ...(opts?.projectManagerEmail && {
1571
+ projectManagerEmail: opts.projectManagerEmail,
1494
1572
  }),
1495
1573
  ...(opts?.wonOverrideReason && {
1496
1574
  wonOverrideReason: opts.wonOverrideReason,
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import "./load-env.js";
3
3
  import { requireApiKey } from "./api-client.js";
4
4
  import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
5
- import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, revokeApiKeyForUser, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
5
+ import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, revokeApiKeyForUser, stageBillAttachmentsFromPaths, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, } from "./commands.js";
6
6
  import { getFlag, parseArgs } from "./parse-args.js";
7
7
  import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
8
8
  import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
@@ -145,7 +145,7 @@ Budgets
145
145
  create-budget-approval <budgetId> (also sends approval request emails)
146
146
  create-estimate <budgetId> (creates QuickBooks estimate; supports preflight updates)
147
147
  add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
148
- update-budget-item --id <budgetItemId> [--description plain-text] [--note plain-text] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
148
+ update-budget-item --id <budgetItemId> [--description plain-text] [--note plain-text] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false] [--gstOutOfScope true|false]
149
149
  remove-budget-item <budgetItemId>
150
150
  reorder-budget-items <budgetId> --itemIds <csv>
151
151
  update-budget-item-supplier --budgetItemId --supplierId
@@ -162,11 +162,13 @@ Bills
162
162
  isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
163
163
  New supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for each non-legacy line item.
164
164
  create-bill-approval <billId> (also sends approval request emails)
165
- create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim; use upload-bill-attachment after create to add files)
165
+ create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
166
+ stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
166
167
  update-bill --payload '<json>' (bill.update; must include id)
167
168
  delete-bill <billId>
168
169
  update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
169
170
  status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
171
+ Moving a bill or claim to PAID sends creator and supplier emails, CC m@getout.sg and BCC noah@getout.sg; duplicate claim recipients receive one email.
170
172
  patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId] [--paymentDate <ISO>]
171
173
  Clear a field: --clearPaymentTrackingUrl true | --clearPaymentReference true | --clearQuickbooksBillId true | --clearPaymentDate true
172
174
  patch-bill-invoice-number <billId> <invoiceNumber>
@@ -180,7 +182,7 @@ Quotations
180
182
  list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
181
183
  get-quotation <quotationId>
182
184
  upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
183
- create-quotation --payload '<json>' (quotation.createDraft)
185
+ create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and gstAmount)
184
186
  submit-quotation <quotationId>
185
187
  approve-quotation <quotationId>
186
188
  reject-quotation <quotationId> --reason <text>
@@ -205,11 +207,12 @@ Companies & projects
205
207
  list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
206
208
  Omit --active to include both active and inactive projects.
207
209
  get-project <id>
208
- create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
209
- update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
210
+ Budget overview totalRevenue includes Estimate Accepted and Estimate Closed budgets only.
211
+ create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--projectManagerId <userId>] [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
212
+ update-project --payload '<json>' (project.updateProject; optional projectManagerId and requestQboAccountantNotification; default notify like web)
210
213
  delete-project <id>
211
- update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
212
- Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
214
+ update-project-status <id> <status> [--projectManagerId <userId>] [--projectManagerEmail <email>] [--wonOverrideReason <text>]
215
+ Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
213
216
  Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
214
217
  status: ${projectStatusesForHelp.join(", ")}
215
218
 
@@ -478,6 +481,7 @@ async function main() {
478
481
  unitPrice: parseOptNum(getFlag(flags, "unitPrice"), "--unitPrice"),
479
482
  isFreeOfCharge: parseOptionalBoolFlag(flags, "isFreeOfCharge"),
480
483
  gstInclusive: parseOptionalBoolFlag(flags, "gstInclusive"),
484
+ gstOutOfScope: parseOptionalBoolFlag(flags, "gstOutOfScope"),
481
485
  });
482
486
  break;
483
487
  }
@@ -690,6 +694,26 @@ async function main() {
690
694
  await getBillAttachments(id);
691
695
  break;
692
696
  }
697
+ case "stage-bill-attachment": {
698
+ const [projectId, ...positionalFilePaths] = positional;
699
+ const fileFlag = getFlag(flags, "file");
700
+ const filesFlag = getFlag(flags, "files");
701
+ const filePaths = [
702
+ ...positionalFilePaths,
703
+ ...(fileFlag !== undefined ? [String(fileFlag)] : []),
704
+ ...(filesFlag !== undefined
705
+ ? String(filesFlag)
706
+ .split(",")
707
+ .map((value) => value.trim())
708
+ .filter(Boolean)
709
+ : []),
710
+ ];
711
+ if (!projectId || filePaths.length === 0) {
712
+ throw new Error("stage-bill-attachment requires <projectId> <filePath...> or --file/--files");
713
+ }
714
+ await stageBillAttachmentsFromPaths(projectId, filePaths);
715
+ break;
716
+ }
693
717
  case "upload-bill-attachment":
694
718
  case "attach-bill-attachment":
695
719
  case "attach-bill-payment-receipt": {
@@ -976,6 +1000,7 @@ async function main() {
976
1000
  const contactPersonId = getFlag(flags, "contactPersonId");
977
1001
  const insideSalesId = getFlag(flags, "insideSalesId");
978
1002
  const businessDevelopmentId = getFlag(flags, "businessDevelopmentId");
1003
+ const projectManagerId = getFlag(flags, "projectManagerId");
979
1004
  const venue = getFlag(flags, "venue");
980
1005
  const pax = getFlag(flags, "pax");
981
1006
  const asanaTaskId = getFlag(flags, "asanaTaskId");
@@ -994,7 +1019,7 @@ async function main() {
994
1019
  !businessDevelopmentId ||
995
1020
  !venue ||
996
1021
  !startDate) {
997
- throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate (ISO/datetime flags as appropriate); optional --asanaTaskId, --asanaSearch, --pax and --endDate");
1022
+ throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate (ISO/datetime flags as appropriate); optional --projectManagerId, --asanaTaskId, --asanaSearch, --pax and --endDate");
998
1023
  }
999
1024
  await createProject({
1000
1025
  name,
@@ -1002,6 +1027,7 @@ async function main() {
1002
1027
  contactPersonId,
1003
1028
  insideSalesId,
1004
1029
  businessDevelopmentId,
1030
+ projectManagerId,
1005
1031
  venue,
1006
1032
  pax,
1007
1033
  asanaTaskId,
@@ -1037,11 +1063,13 @@ async function main() {
1037
1063
  throw new Error("update-project-status requires <id> <status>");
1038
1064
  }
1039
1065
  const pmIdFlag = getFlag(flags, "projectManagerId");
1040
- const pmNameFlag = getFlag(flags, "projectManagerName");
1066
+ const pmEmailFlag = getFlag(flags, "projectManagerEmail");
1041
1067
  const wonOverrideReasonFlag = getFlag(flags, "wonOverrideReason");
1042
1068
  await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
1043
1069
  ...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
1044
- ...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
1070
+ ...(pmEmailFlag && {
1071
+ projectManagerEmail: String(pmEmailFlag),
1072
+ }),
1045
1073
  ...(wonOverrideReasonFlag && {
1046
1074
  wonOverrideReason: String(wonOverrideReasonFlag),
1047
1075
  }),
@@ -77,8 +77,8 @@ export function parseBudgetDiscountPayload(raw) {
77
77
  totalSellingBeforeDiscount: Number(o.totalSellingBeforeDiscount),
78
78
  };
79
79
  }
80
- const billAttachment = (x, i) => {
81
- const label = `create-bill payload.attachments[${i}]`;
80
+ const billAttachment = (x, i, field = "attachments") => {
81
+ const label = `create-bill payload.${field}[${i}]`;
82
82
  const a = requireObject(x, label);
83
83
  return {
84
84
  id: requiredString(a.id, `${label}.id`),
@@ -102,6 +102,16 @@ export function parseCreateBillPayload(raw) {
102
102
  const attachments = Array.isArray(attachmentsRaw)
103
103
  ? attachmentsRaw.map((x, i) => billAttachment(x, i))
104
104
  : [];
105
+ const alreadyPaidRaw = o.alreadyPaid;
106
+ if (alreadyPaidRaw !== undefined &&
107
+ alreadyPaidRaw !== null &&
108
+ typeof alreadyPaidRaw !== "boolean") {
109
+ throw new Error("create-bill payload.alreadyPaid must be boolean.");
110
+ }
111
+ const paymentProofAttachmentsRaw = o.paymentProofAttachments;
112
+ const paymentProofAttachments = Array.isArray(paymentProofAttachmentsRaw)
113
+ ? paymentProofAttachmentsRaw.map((x, i) => billAttachment(x, i, "paymentProofAttachments"))
114
+ : [];
105
115
  let verificationResults;
106
116
  const vr = o.verificationResults;
107
117
  if (vr !== undefined && vr !== null) {
@@ -153,6 +163,9 @@ export function parseCreateBillPayload(raw) {
153
163
  comment: optionalString(o.comment),
154
164
  isClaimable,
155
165
  attachments,
166
+ alreadyPaid: alreadyPaidRaw ?? undefined,
167
+ paymentReference: optionalString(o.paymentReference),
168
+ paymentProofAttachments,
156
169
  extractedSupplierName: optionalString(o.extractedSupplierName),
157
170
  extractedAmount: o.extractedAmount === undefined || o.extractedAmount === null
158
171
  ? undefined
@@ -214,6 +227,7 @@ export function parseUpdateProjectPayload(raw) {
214
227
  asanaTaskId: requiredString(o.asanaTaskId, "update-project payload.asanaTaskId"),
215
228
  insideSalesId: requiredString(o.insideSalesId, "update-project payload.insideSalesId"),
216
229
  businessDevelopmentId: requiredString(o.businessDevelopmentId, "update-project payload.businessDevelopmentId"),
230
+ projectManagerId: optionalString(o.projectManagerId),
217
231
  venue: requiredString(o.venue, "update-project payload.venue"),
218
232
  pax: optionalString(o.pax),
219
233
  dateRange: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.17.0",
3
+ "version": "1.18.1",
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",