@go-labs-sg/bb 1.16.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 +22 -6
- package/dist/api-client.js +11 -1
- package/dist/commands.js +185 -49
- package/dist/index.js +147 -18
- package/dist/parse-cli-enums.js +12 -1
- package/dist/parse-mutation-payload.js +16 -2
- package/dist/prisma-enums.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,20 @@ export BB_API_KEY=<your-key>
|
|
|
37
37
|
|
|
38
38
|
Create or revoke keys in the web app: **Goracle → API Keys** (MCP API keys).
|
|
39
39
|
|
|
40
|
+
Admins can provision API-only service identities and manage their keys from the
|
|
41
|
+
CLI. These identities are not human web accounts: `create-user` does not create
|
|
42
|
+
or link a Google OAuth account, so the resulting user cannot sign in with Google.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bb create-user --email agent@example.com --name "Budget Agent" --role USER
|
|
46
|
+
bb create-api-key --userId <user-id> --name "Budget Agent CLI"
|
|
47
|
+
bb list-api-keys --userId <user-id>
|
|
48
|
+
bb revoke-api-key <api-key-id> --userId <user-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The raw key is returned only by `create-api-key`; copy it immediately. Listing
|
|
52
|
+
keys returns metadata only.
|
|
53
|
+
|
|
40
54
|
The CLI talks to the production API: `https://budget-builder.getout.events`.
|
|
41
55
|
|
|
42
56
|
## Usage
|
|
@@ -56,10 +70,12 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
56
70
|
|
|
57
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.
|
|
58
72
|
|
|
59
|
-
**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`.
|
|
60
74
|
|
|
61
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.
|
|
62
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
|
+
|
|
63
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.
|
|
64
80
|
|
|
65
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).
|
|
@@ -68,14 +84,14 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
68
84
|
|
|
69
85
|
| Area | Commands (non-exhaustive) |
|
|
70
86
|
| --- | --- |
|
|
71
|
-
| **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
|
|
72
|
-
| **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
|
|
73
|
-
| **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`) |
|
|
74
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` |
|
|
75
|
-
| **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`) |
|
|
76
92
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
77
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) |
|
|
78
|
-
| **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
|
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` |
|
|
79
95
|
| **Errors** | `get-recent-errors`, `get-error-metrics` |
|
|
80
96
|
| **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
|
|
81
97
|
|
package/dist/api-client.js
CHANGED
|
@@ -8,10 +8,20 @@ function getAuthHeader() {
|
|
|
8
8
|
return undefined;
|
|
9
9
|
return `Bearer ${key.trim()}`;
|
|
10
10
|
}
|
|
11
|
+
const containsRawApiKey = (value, seen = new WeakSet()) => {
|
|
12
|
+
if (typeof value !== "object" || value === null)
|
|
13
|
+
return false;
|
|
14
|
+
if (seen.has(value))
|
|
15
|
+
return false;
|
|
16
|
+
seen.add(value);
|
|
17
|
+
return Object.entries(value).some(([key, nestedValue]) => (key.toLowerCase() === "apikey" && typeof nestedValue === "string") ||
|
|
18
|
+
containsRawApiKey(nestedValue, seen));
|
|
19
|
+
};
|
|
11
20
|
export const api = createTRPCProxyClient({
|
|
12
21
|
links: [
|
|
13
22
|
loggerLink({
|
|
14
|
-
enabled: () => shouldLogCliActions()
|
|
23
|
+
enabled: (opts) => shouldLogCliActions() &&
|
|
24
|
+
!(opts.direction === "down" && containsRawApiKey(opts.result)),
|
|
15
25
|
colorMode: "ansi",
|
|
16
26
|
withContext: false,
|
|
17
27
|
// tRPC defaults use console.log for requests; stderr keeps stdout JSON-safe for pipes.
|
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,30 +638,104 @@ export async function getBillAttachments(id) {
|
|
|
589
638
|
const result = await api.bill.getAttachments.query({ id });
|
|
590
639
|
out(result);
|
|
591
640
|
}
|
|
592
|
-
export async function
|
|
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
|
+
}
|
|
671
|
+
export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
|
|
672
|
+
const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
|
|
673
|
+
out(confirmed);
|
|
674
|
+
}
|
|
675
|
+
const uploadSingleBillAttachmentFromPath = async (billId, filePath, type = "BILL") => {
|
|
593
676
|
const buf = await readFile(filePath);
|
|
594
677
|
const fileName = basename(filePath);
|
|
595
678
|
const size = buf.byteLength;
|
|
679
|
+
const attachmentContentType = billAttachmentContentTypeForFileName(fileName);
|
|
596
680
|
const { uploadUrl, key } = await api.attachment.requestBillAttachmentUpload.mutate({
|
|
597
681
|
billId,
|
|
682
|
+
contentType: attachmentContentType,
|
|
598
683
|
fileName,
|
|
599
684
|
size,
|
|
600
685
|
});
|
|
601
686
|
const res = await fetch(uploadUrl, {
|
|
602
687
|
method: "PUT",
|
|
603
688
|
body: buf,
|
|
604
|
-
headers: { "Content-Type":
|
|
689
|
+
headers: { "Content-Type": attachmentContentType },
|
|
605
690
|
});
|
|
606
691
|
if (!res.ok) {
|
|
607
692
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
608
693
|
}
|
|
609
|
-
|
|
694
|
+
return await api.attachment.confirmBillAttachment.mutate({
|
|
610
695
|
billId,
|
|
611
696
|
key,
|
|
612
697
|
name: fileName,
|
|
613
698
|
size,
|
|
699
|
+
type,
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
export async function uploadBillAttachmentsFromPaths(billId, filePaths, type = "BILL") {
|
|
703
|
+
if (filePaths.length === 0) {
|
|
704
|
+
throw new Error("At least one bill attachment file path is required");
|
|
705
|
+
}
|
|
706
|
+
const results = [];
|
|
707
|
+
for (const filePath of filePaths) {
|
|
708
|
+
results.push(await uploadSingleBillAttachmentFromPath(billId, filePath, type));
|
|
709
|
+
}
|
|
710
|
+
out({
|
|
711
|
+
success: true,
|
|
712
|
+
billId,
|
|
713
|
+
type,
|
|
714
|
+
count: results.length,
|
|
715
|
+
attachments: results.map((result) => result.attachment),
|
|
716
|
+
results,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [], paymentProofPaths = [], }) {
|
|
720
|
+
if (invoicePaths.length === 0 && paymentProofPaths.length === 0) {
|
|
721
|
+
throw new Error("At least one invoice or payment proof file path is required");
|
|
722
|
+
}
|
|
723
|
+
const invoiceResults = [];
|
|
724
|
+
for (const filePath of invoicePaths) {
|
|
725
|
+
invoiceResults.push(await uploadSingleBillAttachmentFromPath(billId, filePath, "BILL"));
|
|
726
|
+
}
|
|
727
|
+
const paymentProofResults = [];
|
|
728
|
+
for (const filePath of paymentProofPaths) {
|
|
729
|
+
paymentProofResults.push(await uploadSingleBillAttachmentFromPath(billId, filePath, "BILL_PAYMENT_PROOF"));
|
|
730
|
+
}
|
|
731
|
+
out({
|
|
732
|
+
success: true,
|
|
733
|
+
billId,
|
|
734
|
+
count: invoiceResults.length + paymentProofResults.length,
|
|
735
|
+
invoiceAttachments: invoiceResults.map((result) => result.attachment),
|
|
736
|
+
paymentProofAttachments: paymentProofResults.map((result) => result.attachment),
|
|
737
|
+
results: [...invoiceResults, ...paymentProofResults],
|
|
614
738
|
});
|
|
615
|
-
out(confirmed);
|
|
616
739
|
}
|
|
617
740
|
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
618
741
|
const buf = await readFile(filePath);
|
|
@@ -646,15 +769,17 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
|
646
769
|
const buf = await readFile(filePath);
|
|
647
770
|
const fileName = basename(filePath);
|
|
648
771
|
const size = buf.byteLength;
|
|
772
|
+
const attachmentContentType = attachmentContentTypeForFileName(fileName);
|
|
649
773
|
const { uploadUrl, key } = await api.attachment.requestBudgetAttachmentUpload.mutate({
|
|
650
774
|
budgetId,
|
|
775
|
+
contentType: attachmentContentType,
|
|
651
776
|
fileName,
|
|
652
777
|
size,
|
|
653
778
|
});
|
|
654
779
|
const res = await fetch(uploadUrl, {
|
|
655
780
|
method: "PUT",
|
|
656
781
|
body: buf,
|
|
657
|
-
headers: { "Content-Type":
|
|
782
|
+
headers: { "Content-Type": attachmentContentType },
|
|
658
783
|
});
|
|
659
784
|
if (!res.ok) {
|
|
660
785
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
@@ -677,15 +802,17 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
|
677
802
|
const buf = await readFile(filePath);
|
|
678
803
|
const fileName = basename(filePath);
|
|
679
804
|
const size = buf.byteLength;
|
|
805
|
+
const attachmentContentType = attachmentContentTypeForFileName(fileName);
|
|
680
806
|
const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
|
|
681
807
|
budgetId,
|
|
808
|
+
contentType: attachmentContentType,
|
|
682
809
|
fileName,
|
|
683
810
|
size,
|
|
684
811
|
});
|
|
685
812
|
const res = await fetch(uploadUrl, {
|
|
686
813
|
method: "PUT",
|
|
687
814
|
body: buf,
|
|
688
|
-
headers: { "Content-Type":
|
|
815
|
+
headers: { "Content-Type": attachmentContentType },
|
|
689
816
|
});
|
|
690
817
|
if (!res.ok) {
|
|
691
818
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
@@ -774,19 +901,15 @@ export async function createBudgetApproval(budgetId, preflight) {
|
|
|
774
901
|
const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
|
|
775
902
|
let email;
|
|
776
903
|
if (approvalIds.length === 0) {
|
|
777
|
-
email =
|
|
904
|
+
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
778
905
|
}
|
|
779
906
|
else {
|
|
780
907
|
try {
|
|
781
|
-
await api.email.sendApprovalRequestEmail.mutate(approvalIds);
|
|
782
|
-
email =
|
|
908
|
+
const delivery = await api.email.sendApprovalRequestEmail.mutate(approvalIds);
|
|
909
|
+
email = createApprovalEmailDeliveryReport(delivery);
|
|
783
910
|
}
|
|
784
911
|
catch (error) {
|
|
785
|
-
email =
|
|
786
|
-
sent: false,
|
|
787
|
-
count: approvalIds.length,
|
|
788
|
-
error: error instanceof Error ? error.message : String(error),
|
|
789
|
-
};
|
|
912
|
+
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
790
913
|
}
|
|
791
914
|
}
|
|
792
915
|
out({
|
|
@@ -967,19 +1090,15 @@ export async function createSupplierFromPayload(raw) {
|
|
|
967
1090
|
let emailResult;
|
|
968
1091
|
const approvalIds = (approval.results ?? []).map((item) => ({ id: item.id }));
|
|
969
1092
|
if (approvalIds.length === 0) {
|
|
970
|
-
emailResult =
|
|
1093
|
+
emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
971
1094
|
}
|
|
972
1095
|
else {
|
|
973
1096
|
try {
|
|
974
|
-
await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
|
|
975
|
-
emailResult =
|
|
1097
|
+
const delivery = await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
|
|
1098
|
+
emailResult = createApprovalEmailDeliveryReport(delivery);
|
|
976
1099
|
}
|
|
977
1100
|
catch (error) {
|
|
978
|
-
emailResult =
|
|
979
|
-
sent: false,
|
|
980
|
-
count: approvalIds.length,
|
|
981
|
-
error: error instanceof Error ? error.message : String(error),
|
|
982
|
-
};
|
|
1101
|
+
emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
983
1102
|
}
|
|
984
1103
|
}
|
|
985
1104
|
out({
|
|
@@ -1016,27 +1135,23 @@ export async function updateSupplierFromPayload(raw) {
|
|
|
1016
1135
|
approvalResult = {
|
|
1017
1136
|
requested: true,
|
|
1018
1137
|
approvalCount: 0,
|
|
1019
|
-
email:
|
|
1138
|
+
email: createApprovalEmailDeliveryFailure("No approval ids returned"),
|
|
1020
1139
|
};
|
|
1021
1140
|
}
|
|
1022
1141
|
else {
|
|
1023
1142
|
try {
|
|
1024
|
-
await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
|
|
1143
|
+
const delivery = await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
|
|
1025
1144
|
approvalResult = {
|
|
1026
1145
|
requested: true,
|
|
1027
1146
|
approvalCount: approvalIds.length,
|
|
1028
|
-
email:
|
|
1147
|
+
email: createApprovalEmailDeliveryReport(delivery),
|
|
1029
1148
|
};
|
|
1030
1149
|
}
|
|
1031
1150
|
catch (error) {
|
|
1032
1151
|
approvalResult = {
|
|
1033
1152
|
requested: true,
|
|
1034
1153
|
approvalCount: approvalIds.length,
|
|
1035
|
-
email:
|
|
1036
|
-
sent: false,
|
|
1037
|
-
count: approvalIds.length,
|
|
1038
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1039
|
-
},
|
|
1154
|
+
email: createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error)),
|
|
1040
1155
|
};
|
|
1041
1156
|
}
|
|
1042
1157
|
}
|
|
@@ -1089,7 +1204,7 @@ export async function approveBudget(budgetId) {
|
|
|
1089
1204
|
entity: `budget ${budgetId}`,
|
|
1090
1205
|
details: "and send requester reply email",
|
|
1091
1206
|
});
|
|
1092
|
-
const notifications = await
|
|
1207
|
+
const notifications = await getAllNotifications();
|
|
1093
1208
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1094
1209
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1095
1210
|
n.type === ExtendedApprovalType.BUDGET &&
|
|
@@ -1125,7 +1240,7 @@ export async function rejectBudget(budgetId, reason) {
|
|
|
1125
1240
|
entity: `budget ${budgetId}`,
|
|
1126
1241
|
details: `with reason "${reason}" and send requester reply email`,
|
|
1127
1242
|
});
|
|
1128
|
-
const notifications = await
|
|
1243
|
+
const notifications = await getAllNotifications();
|
|
1129
1244
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1130
1245
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1131
1246
|
n.type === ExtendedApprovalType.BUDGET &&
|
|
@@ -1223,7 +1338,7 @@ export async function approveSupplier(supplierId) {
|
|
|
1223
1338
|
entity: `supplier ${supplierId}`,
|
|
1224
1339
|
details: "and send requester reply email",
|
|
1225
1340
|
});
|
|
1226
|
-
const notifications = await
|
|
1341
|
+
const notifications = await getAllNotifications();
|
|
1227
1342
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1228
1343
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1229
1344
|
n.type === ExtendedApprovalType.SUPPLIER &&
|
|
@@ -1261,7 +1376,7 @@ export async function rejectSupplier(supplierId, reason) {
|
|
|
1261
1376
|
entity: `supplier ${supplierId}`,
|
|
1262
1377
|
details: `with reason "${reason}" and send requester reply email`,
|
|
1263
1378
|
});
|
|
1264
|
-
const notifications = await
|
|
1379
|
+
const notifications = await getAllNotifications();
|
|
1265
1380
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1266
1381
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1267
1382
|
n.type === ExtendedApprovalType.SUPPLIER &&
|
|
@@ -1300,7 +1415,7 @@ export async function rejectBill(billId, reason) {
|
|
|
1300
1415
|
entity: `bill ${billId}`,
|
|
1301
1416
|
details: `with reason "${reason}" and send requester reply email`,
|
|
1302
1417
|
});
|
|
1303
|
-
const notifications = await
|
|
1418
|
+
const notifications = await getAllNotifications();
|
|
1304
1419
|
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1305
1420
|
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1306
1421
|
n.type === ExtendedApprovalType.BILL &&
|
|
@@ -1314,6 +1429,10 @@ export async function rejectBill(billId, reason) {
|
|
|
1314
1429
|
status: ExtendedApprovalStatus.REJECTED,
|
|
1315
1430
|
rejectionReason: reason,
|
|
1316
1431
|
});
|
|
1432
|
+
if (result.processing) {
|
|
1433
|
+
out(result);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1317
1436
|
let email;
|
|
1318
1437
|
try {
|
|
1319
1438
|
await api.email.sendReplyBillApprovalEmail.mutate(result.updatedApproval);
|
|
@@ -1335,7 +1454,7 @@ export async function approveQuotation(quotationId) {
|
|
|
1335
1454
|
action: "Approve quotation",
|
|
1336
1455
|
entity: `quotation ${quotationId}`,
|
|
1337
1456
|
});
|
|
1338
|
-
const notifications = await
|
|
1457
|
+
const notifications = await getAllNotifications();
|
|
1339
1458
|
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1340
1459
|
n.type === QUOTATION &&
|
|
1341
1460
|
(n.supplierQuotationId === quotationId ||
|
|
@@ -1355,7 +1474,7 @@ export async function rejectQuotation(quotationId, reason) {
|
|
|
1355
1474
|
entity: `quotation ${quotationId}`,
|
|
1356
1475
|
details: `with reason "${reason}"`,
|
|
1357
1476
|
});
|
|
1358
|
-
const notifications = await
|
|
1477
|
+
const notifications = await getAllNotifications();
|
|
1359
1478
|
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1360
1479
|
n.type === QUOTATION &&
|
|
1361
1480
|
(n.supplierQuotationId === quotationId ||
|
|
@@ -1424,6 +1543,7 @@ export async function createProject(opts) {
|
|
|
1424
1543
|
contactPersonId: opts.contactPersonId,
|
|
1425
1544
|
insideSalesId: opts.insideSalesId,
|
|
1426
1545
|
businessDevelopmentId: opts.businessDevelopmentId,
|
|
1546
|
+
projectManagerId: opts.projectManagerId,
|
|
1427
1547
|
venue: opts.venue,
|
|
1428
1548
|
pax: opts.pax,
|
|
1429
1549
|
asanaTaskId: links.asanaTaskId,
|
|
@@ -1447,8 +1567,8 @@ export async function updateProjectStatus(id, status, opts) {
|
|
|
1447
1567
|
id,
|
|
1448
1568
|
status,
|
|
1449
1569
|
...(opts?.projectManagerId && { projectManagerId: opts.projectManagerId }),
|
|
1450
|
-
...(opts?.
|
|
1451
|
-
|
|
1570
|
+
...(opts?.projectManagerEmail && {
|
|
1571
|
+
projectManagerEmail: opts.projectManagerEmail,
|
|
1452
1572
|
}),
|
|
1453
1573
|
...(opts?.wonOverrideReason && {
|
|
1454
1574
|
wonOverrideReason: opts.wonOverrideReason,
|
|
@@ -1495,6 +1615,22 @@ export async function getSupplierAnalytics(opts) {
|
|
|
1495
1615
|
out(result);
|
|
1496
1616
|
}
|
|
1497
1617
|
// --- Dashboard / analytics ---
|
|
1618
|
+
export async function createUser(input) {
|
|
1619
|
+
const user = await api.user.create.mutate(input);
|
|
1620
|
+
out(user);
|
|
1621
|
+
}
|
|
1622
|
+
export async function createApiKeyForUser(input) {
|
|
1623
|
+
const apiKey = await api.mcpApiKey.create.mutate(input);
|
|
1624
|
+
out(apiKey);
|
|
1625
|
+
}
|
|
1626
|
+
export async function listApiKeysForUser(input) {
|
|
1627
|
+
const apiKeys = await api.mcpApiKey.list.query(input);
|
|
1628
|
+
out(apiKeys);
|
|
1629
|
+
}
|
|
1630
|
+
export async function revokeApiKeyForUser(input) {
|
|
1631
|
+
const result = await api.mcpApiKey.revoke.mutate(input);
|
|
1632
|
+
out(result);
|
|
1633
|
+
}
|
|
1498
1634
|
export async function listUsers() {
|
|
1499
1635
|
const users = await api.user.getAllUsers.query();
|
|
1500
1636
|
out(users);
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import "./load-env.js";
|
|
3
3
|
import { requireApiKey } from "./api-client.js";
|
|
4
4
|
import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
|
|
5
|
-
import { addBudgetItems, approveBill, approveBudget, approveQuotation, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, 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, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, rejectBill, rejectBudget, rejectQuotation, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, submitQuotation, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, 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
|
-
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
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";
|
|
9
9
|
function parsePositiveIntFlag(value, flagName) {
|
|
10
10
|
if (value === undefined)
|
|
@@ -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,23 +162,27 @@ 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>
|
|
173
175
|
get-bill-attachments <billId>
|
|
174
|
-
upload-bill-attachment <billId> <filePath>
|
|
176
|
+
upload-bill-attachment <billId> <filePath...> [--file <path>] [--files <csv>] Attach invoice/supporting files as BILL attachments to any bill status, including PAID bills
|
|
177
|
+
attach-bill-payment-receipt <billId> <filePath...> [--file <path>] [--files <csv>] Attach Wise/payment-proof PDFs as BILL_PAYMENT_PROOF attachments
|
|
178
|
+
attach-bill-documents <billId> [--invoice <path>] [--invoices <csv>] [--paymentProof|--payment-proof|--proof <path>] [--paymentProofs|--payment-proofs|--proofs <csv>] Attach invoice and/or payment proof files with separate BB attachment types
|
|
175
179
|
get-bill-details <billId>
|
|
176
180
|
|
|
177
181
|
Quotations
|
|
178
182
|
list-quotations [--projectId] [--budgetId] [--supplierId] [--status CSV] [--page] [--pageSize]
|
|
179
183
|
get-quotation <quotationId>
|
|
180
184
|
upload-quotation-attachment <projectId> <filePath> Presigned S3 upload; returns attachment JSON for create-quotation payload
|
|
181
|
-
create-quotation --payload '<json>' (quotation.createDraft)
|
|
185
|
+
create-quotation --payload '<json>' (quotation.createDraft; requires amountWithoutGst and gstAmount)
|
|
182
186
|
submit-quotation <quotationId>
|
|
183
187
|
approve-quotation <quotationId>
|
|
184
188
|
reject-quotation <quotationId> --reason <text>
|
|
@@ -203,11 +207,12 @@ Companies & projects
|
|
|
203
207
|
list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
|
|
204
208
|
Omit --active to include both active and inactive projects.
|
|
205
209
|
get-project <id>
|
|
206
|
-
|
|
207
|
-
|
|
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)
|
|
208
213
|
delete-project <id>
|
|
209
|
-
update-project-status <id> <status> [--projectManagerId <
|
|
210
|
-
|
|
214
|
+
update-project-status <id> <status> [--projectManagerId <userId>] [--projectManagerEmail <email>] [--wonOverrideReason <text>]
|
|
215
|
+
Required when moving PITCH → WON: supply --projectManagerId or --projectManagerEmail.
|
|
211
216
|
Required when moving to WON without accepted/closed budget or win proof: --wonOverrideReason (10+ chars).
|
|
212
217
|
status: ${projectStatusesForHelp.join(", ")}
|
|
213
218
|
|
|
@@ -241,6 +246,10 @@ Suppliers & items
|
|
|
241
246
|
|
|
242
247
|
Dashboard & users
|
|
243
248
|
list-users
|
|
249
|
+
create-user --email <email> [--name <name>] [--role ${userRolesForHelp.join("|")}] (admin; API-only service identity with no Google sign-in)
|
|
250
|
+
create-api-key --userId <userId> --name <label> (admin; raw key is shown once)
|
|
251
|
+
list-api-keys [--userId <userId>] (admin; defaults to the caller)
|
|
252
|
+
revoke-api-key <apiKeyId> [--userId <userId>] (admin; --userId is required for another user's key)
|
|
244
253
|
get-user-performance [--userId]
|
|
245
254
|
get-dashboard [--userId] [--role BD|CREATOR|INSIDE_SALES|ALL] [--deals ALL|SUCCESSFUL|LOST] [--timeFrame] [--startDate] [--endDate]
|
|
246
255
|
get-monthly-metrics [same optional flags as get-dashboard] (dashboard.getMonthlyMetrics)
|
|
@@ -472,6 +481,7 @@ async function main() {
|
|
|
472
481
|
unitPrice: parseOptNum(getFlag(flags, "unitPrice"), "--unitPrice"),
|
|
473
482
|
isFreeOfCharge: parseOptionalBoolFlag(flags, "isFreeOfCharge"),
|
|
474
483
|
gstInclusive: parseOptionalBoolFlag(flags, "gstInclusive"),
|
|
484
|
+
gstOutOfScope: parseOptionalBoolFlag(flags, "gstOutOfScope"),
|
|
475
485
|
});
|
|
476
486
|
break;
|
|
477
487
|
}
|
|
@@ -684,12 +694,91 @@ async function main() {
|
|
|
684
694
|
await getBillAttachments(id);
|
|
685
695
|
break;
|
|
686
696
|
}
|
|
687
|
-
case "
|
|
688
|
-
const [
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
}
|
|
717
|
+
case "upload-bill-attachment":
|
|
718
|
+
case "attach-bill-attachment":
|
|
719
|
+
case "attach-bill-payment-receipt": {
|
|
720
|
+
const [billId, ...positionalFilePaths] = positional;
|
|
721
|
+
const fileFlag = getFlag(flags, "file");
|
|
722
|
+
const filesFlag = getFlag(flags, "files");
|
|
723
|
+
const filePaths = [
|
|
724
|
+
...positionalFilePaths,
|
|
725
|
+
...(fileFlag !== undefined ? [String(fileFlag)] : []),
|
|
726
|
+
...(filesFlag !== undefined
|
|
727
|
+
? String(filesFlag)
|
|
728
|
+
.split(",")
|
|
729
|
+
.map((value) => value.trim())
|
|
730
|
+
.filter(Boolean)
|
|
731
|
+
: []),
|
|
732
|
+
];
|
|
733
|
+
if (!billId || filePaths.length === 0) {
|
|
734
|
+
throw new Error(`${cmd} requires <billId> <filePath...> or --file/--files`);
|
|
735
|
+
}
|
|
736
|
+
const attachmentType = cmd === "attach-bill-payment-receipt" ? "BILL_PAYMENT_PROOF" : "BILL";
|
|
737
|
+
const [firstFilePath] = filePaths;
|
|
738
|
+
if (filePaths.length === 1 && firstFilePath !== undefined) {
|
|
739
|
+
await uploadBillAttachmentFromPath(billId, firstFilePath, attachmentType);
|
|
691
740
|
}
|
|
692
|
-
|
|
741
|
+
else {
|
|
742
|
+
await uploadBillAttachmentsFromPaths(billId, filePaths, attachmentType);
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
case "attach-bill-documents": {
|
|
747
|
+
const billId = positional[0];
|
|
748
|
+
if (!billId)
|
|
749
|
+
throw new Error("attach-bill-documents requires <billId>");
|
|
750
|
+
const getFlagAlias = (...flagNames) => {
|
|
751
|
+
for (const flagName of flagNames) {
|
|
752
|
+
const value = getFlag(flags, flagName);
|
|
753
|
+
if (value !== undefined)
|
|
754
|
+
return value;
|
|
755
|
+
}
|
|
756
|
+
return undefined;
|
|
757
|
+
};
|
|
758
|
+
const parseFilesFlag = (...flagNames) => {
|
|
759
|
+
const raw = getFlagAlias(...flagNames);
|
|
760
|
+
if (raw === undefined)
|
|
761
|
+
return [];
|
|
762
|
+
return String(raw)
|
|
763
|
+
.split(",")
|
|
764
|
+
.map((value) => value.trim())
|
|
765
|
+
.filter(Boolean);
|
|
766
|
+
};
|
|
767
|
+
const invoicePath = getFlagAlias("invoice", "invoice-file");
|
|
768
|
+
const paymentProofPath = getFlagAlias("paymentProof", "payment-proof", "proof", "receipt");
|
|
769
|
+
const invoicePaths = [
|
|
770
|
+
...(invoicePath !== undefined ? [String(invoicePath)] : []),
|
|
771
|
+
...parseFilesFlag("invoices", "invoice-files"),
|
|
772
|
+
];
|
|
773
|
+
const paymentProofPaths = [
|
|
774
|
+
...(paymentProofPath !== undefined ? [String(paymentProofPath)] : []),
|
|
775
|
+
...parseFilesFlag("paymentProofs", "payment-proofs", "proofs", "receipts"),
|
|
776
|
+
];
|
|
777
|
+
await uploadBillDocumentsFromPaths({
|
|
778
|
+
billId,
|
|
779
|
+
invoicePaths,
|
|
780
|
+
paymentProofPaths,
|
|
781
|
+
});
|
|
693
782
|
break;
|
|
694
783
|
}
|
|
695
784
|
case "get-bill-details": {
|
|
@@ -911,6 +1000,7 @@ async function main() {
|
|
|
911
1000
|
const contactPersonId = getFlag(flags, "contactPersonId");
|
|
912
1001
|
const insideSalesId = getFlag(flags, "insideSalesId");
|
|
913
1002
|
const businessDevelopmentId = getFlag(flags, "businessDevelopmentId");
|
|
1003
|
+
const projectManagerId = getFlag(flags, "projectManagerId");
|
|
914
1004
|
const venue = getFlag(flags, "venue");
|
|
915
1005
|
const pax = getFlag(flags, "pax");
|
|
916
1006
|
const asanaTaskId = getFlag(flags, "asanaTaskId");
|
|
@@ -929,7 +1019,7 @@ async function main() {
|
|
|
929
1019
|
!businessDevelopmentId ||
|
|
930
1020
|
!venue ||
|
|
931
1021
|
!startDate) {
|
|
932
|
-
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");
|
|
933
1023
|
}
|
|
934
1024
|
await createProject({
|
|
935
1025
|
name,
|
|
@@ -937,6 +1027,7 @@ async function main() {
|
|
|
937
1027
|
contactPersonId,
|
|
938
1028
|
insideSalesId,
|
|
939
1029
|
businessDevelopmentId,
|
|
1030
|
+
projectManagerId,
|
|
940
1031
|
venue,
|
|
941
1032
|
pax,
|
|
942
1033
|
asanaTaskId,
|
|
@@ -972,11 +1063,13 @@ async function main() {
|
|
|
972
1063
|
throw new Error("update-project-status requires <id> <status>");
|
|
973
1064
|
}
|
|
974
1065
|
const pmIdFlag = getFlag(flags, "projectManagerId");
|
|
975
|
-
const
|
|
1066
|
+
const pmEmailFlag = getFlag(flags, "projectManagerEmail");
|
|
976
1067
|
const wonOverrideReasonFlag = getFlag(flags, "wonOverrideReason");
|
|
977
1068
|
await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
|
|
978
1069
|
...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
|
|
979
|
-
...(
|
|
1070
|
+
...(pmEmailFlag && {
|
|
1071
|
+
projectManagerEmail: String(pmEmailFlag),
|
|
1072
|
+
}),
|
|
980
1073
|
...(wonOverrideReasonFlag && {
|
|
981
1074
|
wonOverrideReason: String(wonOverrideReasonFlag),
|
|
982
1075
|
}),
|
|
@@ -1171,6 +1264,42 @@ async function main() {
|
|
|
1171
1264
|
await listUsers();
|
|
1172
1265
|
break;
|
|
1173
1266
|
}
|
|
1267
|
+
case "create-user": {
|
|
1268
|
+
const email = getFlag(flags, "email");
|
|
1269
|
+
if (!email) {
|
|
1270
|
+
throw new Error(`create-user requires --email <email> [--name <name>] [--role ${userRolesForHelp.join("|")}]`);
|
|
1271
|
+
}
|
|
1272
|
+
await createUser({
|
|
1273
|
+
email,
|
|
1274
|
+
name: getFlag(flags, "name"),
|
|
1275
|
+
role: parseUserRole(getFlag(flags, "role")),
|
|
1276
|
+
});
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
case "create-api-key": {
|
|
1280
|
+
const userId = getFlag(flags, "userId");
|
|
1281
|
+
const name = getFlag(flags, "name");
|
|
1282
|
+
if (!userId || !name) {
|
|
1283
|
+
throw new Error("create-api-key requires --userId <userId> --name <label>");
|
|
1284
|
+
}
|
|
1285
|
+
await createApiKeyForUser({ userId, name });
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
case "list-api-keys": {
|
|
1289
|
+
await listApiKeysForUser({ userId: getFlag(flags, "userId") });
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
case "revoke-api-key": {
|
|
1293
|
+
const id = positional[0];
|
|
1294
|
+
if (!id) {
|
|
1295
|
+
throw new Error("revoke-api-key requires <apiKeyId> [--userId <userId>]");
|
|
1296
|
+
}
|
|
1297
|
+
await revokeApiKeyForUser({
|
|
1298
|
+
id,
|
|
1299
|
+
userId: getFlag(flags, "userId"),
|
|
1300
|
+
});
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1174
1303
|
case "get-user-performance": {
|
|
1175
1304
|
await getUserPerformance(getFlag(flags, "userId"));
|
|
1176
1305
|
break;
|
package/dist/parse-cli-enums.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedBillStatus, ExtendedErrorSeverity, ExtendedErrorStatus, ExtendedProjectStatus, TimeFrame, } from "./filter-enums.js";
|
|
2
|
-
import { BillStatus, BudgetStatus, ProjectStatus } from "./prisma-enums.js";
|
|
2
|
+
import { BillStatus, BudgetStatus, ProjectStatus, UserRole, } from "./prisma-enums.js";
|
|
3
3
|
const BUDGET_STATUS_VALUES = new Set(Object.values(BudgetStatus));
|
|
4
4
|
const BILL_STATUS_VALUES = new Set(Object.values(BillStatus));
|
|
5
5
|
const PROJECT_STATUS_VALUES = new Set(Object.values(ProjectStatus));
|
|
6
|
+
const USER_ROLE_VALUES = new Set(Object.values(UserRole));
|
|
6
7
|
const EXTENDED_PROJECT_STATUS_VALUES = new Set(Object.values(ExtendedProjectStatus));
|
|
7
8
|
const DASHBOARD_ROLE_VALUES = new Set([
|
|
8
9
|
"BD",
|
|
@@ -52,6 +53,15 @@ export function parseProjectStatusForUpdate(raw) {
|
|
|
52
53
|
}
|
|
53
54
|
return raw;
|
|
54
55
|
}
|
|
56
|
+
export function parseUserRole(raw) {
|
|
57
|
+
if (raw === undefined || raw === "")
|
|
58
|
+
return UserRole.USER;
|
|
59
|
+
const normalizedRole = raw.trim().toUpperCase();
|
|
60
|
+
if (!USER_ROLE_VALUES.has(normalizedRole)) {
|
|
61
|
+
throw new Error(`Invalid user role "${raw}". Use one of: ${[...USER_ROLE_VALUES].join(", ")}.`);
|
|
62
|
+
}
|
|
63
|
+
return normalizedRole;
|
|
64
|
+
}
|
|
55
65
|
export function parseOptionalExtendedProjectStatus(raw) {
|
|
56
66
|
if (raw === undefined || raw === "")
|
|
57
67
|
return undefined;
|
|
@@ -233,3 +243,4 @@ export const budgetStatusesForHelp = Object.values(BudgetStatus);
|
|
|
233
243
|
export const billStatusesForHelp = Object.values(BillStatus);
|
|
234
244
|
export const billStatusesForUpdateHelp = Object.values(BillStatus).filter((s) => s !== BillStatus.PENDING_APPROVAL);
|
|
235
245
|
export const projectStatusesForHelp = Object.values(ProjectStatus);
|
|
246
|
+
export const userRolesForHelp = Object.values(UserRole);
|
|
@@ -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/dist/prisma-enums.js
CHANGED
|
@@ -36,6 +36,13 @@ export const ApprovalType = {
|
|
|
36
36
|
CUSTOMER_INVOICE: "CUSTOMER_INVOICE",
|
|
37
37
|
QUOTATION: "QUOTATION",
|
|
38
38
|
};
|
|
39
|
+
export const UserRole = {
|
|
40
|
+
USER: "USER",
|
|
41
|
+
LEAD: "LEAD",
|
|
42
|
+
ADMIN: "ADMIN",
|
|
43
|
+
INSIDE_SALES: "INSIDE_SALES",
|
|
44
|
+
ACCOUNTING_TEAM: "ACCOUNTING_TEAM",
|
|
45
|
+
};
|
|
39
46
|
export const ErrorSeverity = {
|
|
40
47
|
LOW: "LOW",
|
|
41
48
|
MEDIUM: "MEDIUM",
|
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",
|