@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 +7 -5
- package/dist/commands.js +124 -46
- package/dist/index.js +39 -11
- package/dist/parse-mutation-payload.js +16 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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 `--
|
|
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
|
|
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
|
|
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 =
|
|
580
|
+
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
528
581
|
}
|
|
529
582
|
else {
|
|
530
583
|
try {
|
|
531
|
-
await api.email.sendBillApprovalRequestEmail.mutate(approvalIds);
|
|
532
|
-
email =
|
|
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":
|
|
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":
|
|
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":
|
|
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 =
|
|
904
|
+
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
820
905
|
}
|
|
821
906
|
else {
|
|
822
907
|
try {
|
|
823
|
-
await api.email.sendApprovalRequestEmail.mutate(approvalIds);
|
|
824
|
-
email =
|
|
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 =
|
|
1093
|
+
emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
1013
1094
|
}
|
|
1014
1095
|
else {
|
|
1015
1096
|
try {
|
|
1016
|
-
await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
|
|
1017
|
-
emailResult =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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?.
|
|
1493
|
-
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
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 <
|
|
212
|
-
|
|
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
|
|
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
|
-
...(
|
|
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
|
|
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.
|
|
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",
|