@go-labs-sg/bb 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/commands.js +141 -5
- package/dist/index.js +68 -7
- package/dist/parse-mutation-payload.js +18 -9
- package/dist/prisma-enums.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
50
50
|
|
|
51
51
|
**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.
|
|
52
52
|
|
|
53
|
-
**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.
|
|
53
|
+
**Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
|
|
54
54
|
|
|
55
55
|
### Command overview
|
|
56
56
|
|
|
@@ -59,7 +59,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
59
59
|
| **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status`, `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
60
60
|
| **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
61
61
|
| **Approvals** | `list-approvals` / `get-pending-approvals`, `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email) |
|
|
62
|
-
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (`--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`,
|
|
62
|
+
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--pax`, `--asanaTaskId`, `--slackChannelId`, `--slackChannelUrl`, `--slackChannelName`, `--startDate` as ISO datetime for project/window start; optional `--endDate`; optional `--description`; optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`) |
|
|
63
63
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
64
64
|
| **Suppliers & items** | `list-suppliers`, `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details`, `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
|
|
65
65
|
| **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
package/dist/commands.js
CHANGED
|
@@ -14,6 +14,11 @@ function isPendingRequest(n) {
|
|
|
14
14
|
const out = (data) => {
|
|
15
15
|
console.log(JSON.stringify(data, null, 2));
|
|
16
16
|
};
|
|
17
|
+
const combineDateAndTime = (day, time) => {
|
|
18
|
+
const d = new Date(day);
|
|
19
|
+
d.setHours(time.getHours(), time.getMinutes(), time.getSeconds(), 0);
|
|
20
|
+
return d;
|
|
21
|
+
};
|
|
17
22
|
/** Extension-based MIME, like browser `File.type` when the OS provides it */
|
|
18
23
|
const contentTypeHeaderForFileName = (fileName) => {
|
|
19
24
|
const ct = contentType(fileName);
|
|
@@ -44,12 +49,81 @@ export async function getBudget(id) {
|
|
|
44
49
|
out({ budget, items });
|
|
45
50
|
}
|
|
46
51
|
export async function updateBudgetStatus(budgetId, status) {
|
|
52
|
+
if (status === "ESTIMATE_ACCEPTED") {
|
|
53
|
+
throw new Error("Use mark-budget-won <budgetId> <filePath> so proof upload is included before setting ESTIMATE_ACCEPTED.");
|
|
54
|
+
}
|
|
47
55
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
48
56
|
id: budgetId,
|
|
49
57
|
status,
|
|
50
58
|
});
|
|
51
59
|
out(result);
|
|
52
60
|
}
|
|
61
|
+
const assertBudgetActionReadiness = async ({ budgetId, requireQuickbooksProject, actionLabel, }) => {
|
|
62
|
+
const budget = await api.budget.getBudget.query({ id: budgetId });
|
|
63
|
+
const hasNoItems = budget.budgetItems.length === 0;
|
|
64
|
+
if (hasNoItems) {
|
|
65
|
+
throw new Error(`Cannot ${actionLabel}: there are no line items in this budget.`);
|
|
66
|
+
}
|
|
67
|
+
const hasZeroCostItems = budget.budgetItems.some((item) => Number(item.cost ?? 0) === 0);
|
|
68
|
+
if (hasZeroCostItems) {
|
|
69
|
+
throw new Error(`Cannot ${actionLabel}: all line items must have a cost greater than zero.`);
|
|
70
|
+
}
|
|
71
|
+
const inactiveSuppliers = budget.budgetItems
|
|
72
|
+
.filter((item) => !item.supplier.active)
|
|
73
|
+
.map((item) => item.supplier.name);
|
|
74
|
+
const uniqueInactiveSuppliers = [...new Set(inactiveSuppliers)];
|
|
75
|
+
if (uniqueInactiveSuppliers.length > 0) {
|
|
76
|
+
throw new Error(`Cannot ${actionLabel}: inactive suppliers found (${uniqueInactiveSuppliers.join(", ")}).`);
|
|
77
|
+
}
|
|
78
|
+
if (requireQuickbooksProject && !budget.project.quickbooksProjectId) {
|
|
79
|
+
throw new Error("Cannot create estimate: project is not linked to QuickBooks yet.");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const runBudgetPreflight = async (budgetId, preflight) => {
|
|
83
|
+
if (!preflight)
|
|
84
|
+
return;
|
|
85
|
+
if (preflight.company) {
|
|
86
|
+
await api.company.updateCompany.mutate(preflight.company);
|
|
87
|
+
}
|
|
88
|
+
if (preflight.contactPerson) {
|
|
89
|
+
await api.contactPerson.updateContactPerson.mutate(preflight.contactPerson);
|
|
90
|
+
}
|
|
91
|
+
if (preflight.eventDetails) {
|
|
92
|
+
const budget = await api.budget.getBudget.query({ id: budgetId });
|
|
93
|
+
const project = await api.project.getProjectById.query({
|
|
94
|
+
id: budget.projectId,
|
|
95
|
+
});
|
|
96
|
+
const day = new Date(preflight.eventDetails.date);
|
|
97
|
+
const start = new Date(preflight.eventDetails.startTime);
|
|
98
|
+
const end = new Date(preflight.eventDetails.endTime);
|
|
99
|
+
const payload = {
|
|
100
|
+
id: project.id,
|
|
101
|
+
name: project.name,
|
|
102
|
+
description: project.description ?? undefined,
|
|
103
|
+
dateRange: {
|
|
104
|
+
from: combineDateAndTime(day, start),
|
|
105
|
+
to: combineDateAndTime(day, end),
|
|
106
|
+
},
|
|
107
|
+
companyId: project.companyId,
|
|
108
|
+
contactPersonId: project.contactPersonId,
|
|
109
|
+
asanaTaskId: project.asanaTaskId ?? "",
|
|
110
|
+
insideSalesId: project.insideSalesId ?? "",
|
|
111
|
+
businessDevelopmentId: project.businessDevelopmentId ?? "",
|
|
112
|
+
venue: project.venue ?? "",
|
|
113
|
+
pax: String(project.pax),
|
|
114
|
+
...(project.slackChannelId &&
|
|
115
|
+
project.slackChannelUrl &&
|
|
116
|
+
project.slackChannelName
|
|
117
|
+
? {
|
|
118
|
+
slackChannelId: project.slackChannelId,
|
|
119
|
+
slackChannelUrl: project.slackChannelUrl,
|
|
120
|
+
slackChannelName: project.slackChannelName,
|
|
121
|
+
}
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
124
|
+
await api.project.updateProject.mutate(payload);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
53
127
|
export async function listBills(opts) {
|
|
54
128
|
const input = {
|
|
55
129
|
projectId: opts.projectId,
|
|
@@ -264,6 +338,38 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
|
264
338
|
});
|
|
265
339
|
out(confirmed);
|
|
266
340
|
}
|
|
341
|
+
export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
|
|
342
|
+
const buf = await readFile(filePath);
|
|
343
|
+
const fileName = basename(filePath);
|
|
344
|
+
const size = buf.byteLength;
|
|
345
|
+
const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
|
|
346
|
+
budgetId,
|
|
347
|
+
fileName,
|
|
348
|
+
size,
|
|
349
|
+
});
|
|
350
|
+
const res = await fetch(uploadUrl, {
|
|
351
|
+
method: "PUT",
|
|
352
|
+
body: buf,
|
|
353
|
+
headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
|
|
354
|
+
});
|
|
355
|
+
if (!res.ok) {
|
|
356
|
+
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
357
|
+
}
|
|
358
|
+
await api.attachment.confirmBudgetWinProofAttachment.mutate({
|
|
359
|
+
budgetId,
|
|
360
|
+
key,
|
|
361
|
+
name: fileName,
|
|
362
|
+
size,
|
|
363
|
+
});
|
|
364
|
+
const result = await api.budget.updateBudgetStatus.mutate({
|
|
365
|
+
id: budgetId,
|
|
366
|
+
status: "ESTIMATE_ACCEPTED",
|
|
367
|
+
});
|
|
368
|
+
out(result);
|
|
369
|
+
}
|
|
370
|
+
export async function markBudgetWonWithProof(budgetId, filePath) {
|
|
371
|
+
await uploadBudgetWinProofFromPath(budgetId, filePath);
|
|
372
|
+
}
|
|
267
373
|
export async function getBillDetails(id) {
|
|
268
374
|
const result = await api.bill.getById.query({ id });
|
|
269
375
|
out(result);
|
|
@@ -297,7 +403,13 @@ export async function getBudgetVersions(budgetId) {
|
|
|
297
403
|
});
|
|
298
404
|
out(versions);
|
|
299
405
|
}
|
|
300
|
-
export async function createBudgetApproval(budgetId) {
|
|
406
|
+
export async function createBudgetApproval(budgetId, preflight) {
|
|
407
|
+
await runBudgetPreflight(budgetId, preflight);
|
|
408
|
+
await assertBudgetActionReadiness({
|
|
409
|
+
budgetId,
|
|
410
|
+
requireQuickbooksProject: false,
|
|
411
|
+
actionLabel: "request approval",
|
|
412
|
+
});
|
|
301
413
|
const result = await api.budget.createBudgetApproval.mutate({ budgetId });
|
|
302
414
|
const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
|
|
303
415
|
let email;
|
|
@@ -322,6 +434,16 @@ export async function createBudgetApproval(budgetId) {
|
|
|
322
434
|
email,
|
|
323
435
|
});
|
|
324
436
|
}
|
|
437
|
+
export async function createEstimate(budgetId, preflight) {
|
|
438
|
+
await runBudgetPreflight(budgetId, preflight);
|
|
439
|
+
await assertBudgetActionReadiness({
|
|
440
|
+
budgetId,
|
|
441
|
+
requireQuickbooksProject: true,
|
|
442
|
+
actionLabel: "create estimate",
|
|
443
|
+
});
|
|
444
|
+
const result = await api.quickbooks.createEstimate.mutate({ budgetId });
|
|
445
|
+
out(result);
|
|
446
|
+
}
|
|
325
447
|
export async function getBudgetDetails(budgetId) {
|
|
326
448
|
const detail = await api.budget.getBudgetDetail.query({ id: budgetId });
|
|
327
449
|
out(detail);
|
|
@@ -762,6 +884,10 @@ export async function createProject(opts) {
|
|
|
762
884
|
description: opts.description,
|
|
763
885
|
companyId: opts.companyId,
|
|
764
886
|
contactPersonId: opts.contactPersonId,
|
|
887
|
+
insideSalesId: opts.insideSalesId,
|
|
888
|
+
businessDevelopmentId: opts.businessDevelopmentId,
|
|
889
|
+
venue: opts.venue,
|
|
890
|
+
pax: opts.pax,
|
|
765
891
|
asanaTaskId: opts.asanaTaskId,
|
|
766
892
|
slackChannelId: opts.slackChannelId,
|
|
767
893
|
slackChannelUrl: opts.slackChannelUrl,
|
|
@@ -776,8 +902,15 @@ export async function createProject(opts) {
|
|
|
776
902
|
});
|
|
777
903
|
out(result);
|
|
778
904
|
}
|
|
779
|
-
export async function updateProjectStatus(id, status) {
|
|
780
|
-
const result = await api.project.updateProjectStatus.mutate({
|
|
905
|
+
export async function updateProjectStatus(id, status, opts) {
|
|
906
|
+
const result = await api.project.updateProjectStatus.mutate({
|
|
907
|
+
id,
|
|
908
|
+
status,
|
|
909
|
+
...(opts?.projectManagerId && { projectManagerId: opts.projectManagerId }),
|
|
910
|
+
...(opts?.projectManagerName && {
|
|
911
|
+
projectManagerName: opts.projectManagerName,
|
|
912
|
+
}),
|
|
913
|
+
});
|
|
781
914
|
out(result);
|
|
782
915
|
}
|
|
783
916
|
// --- Contacts ---
|
|
@@ -915,7 +1048,10 @@ export async function getApprovedBudgets(opts) {
|
|
|
915
1048
|
const revenue = calc.totalSellingAfterDiscount ?? 0;
|
|
916
1049
|
const cost = calc.totalCostWithoutGst ?? 0;
|
|
917
1050
|
const gp = calc.gpPercentage ?? 0;
|
|
918
|
-
const
|
|
1051
|
+
const startDate = b.project.startDate;
|
|
1052
|
+
const date = startDate instanceof Date
|
|
1053
|
+
? startDate.toISOString()
|
|
1054
|
+
: String(startDate ?? "");
|
|
919
1055
|
const createdAt = b.createdAt instanceof Date
|
|
920
1056
|
? b.createdAt.toISOString()
|
|
921
1057
|
: String(b.createdAt);
|
|
@@ -925,7 +1061,7 @@ export async function getApprovedBudgets(opts) {
|
|
|
925
1061
|
categoryId: b.categoryId,
|
|
926
1062
|
company: b.project?.company?.name,
|
|
927
1063
|
date,
|
|
928
|
-
pax: b.pax,
|
|
1064
|
+
pax: b.project.pax,
|
|
929
1065
|
revenue: Number(revenue.toFixed(2)),
|
|
930
1066
|
cost: Number(cost.toFixed(2)),
|
|
931
1067
|
gpPercentage: Number(gp.toFixed(2)),
|
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, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
|
|
5
|
+
import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createEstimate, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, markBudgetWonWithProof, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
|
|
6
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
7
7
|
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
8
8
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
@@ -76,10 +76,12 @@ Budgets
|
|
|
76
76
|
get-budget-versions <budgetId>
|
|
77
77
|
update-budget-status <id> <status>
|
|
78
78
|
status: ${budgetStatusesForHelp.join(", ")}
|
|
79
|
-
|
|
79
|
+
mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
|
|
80
|
+
create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
|
|
80
81
|
update-budget --payload '<json>' (budget.updateBudget; must include id)
|
|
81
82
|
delete-budget <budgetId>
|
|
82
83
|
create-budget-approval <budgetId> (also sends approval request emails)
|
|
84
|
+
create-estimate <budgetId> (creates QuickBooks estimate; supports preflight updates)
|
|
83
85
|
add-budget-items --budgetId --items '[{"itemId":"…","quantity":1,"markup":30},…]'
|
|
84
86
|
update-budget-item --id <budgetItemId> [--description] [--note] [--quantity] [--markup] [--cost] [--unitPrice] [--isFreeOfCharge true|false] [--gstInclusive true|false]
|
|
85
87
|
remove-budget-item <budgetItemId>
|
|
@@ -125,10 +127,11 @@ Companies & projects
|
|
|
125
127
|
delete-company <id>
|
|
126
128
|
list-projects [--companyId] [--name] [--status] [--page] [--perPage]
|
|
127
129
|
get-project <id>
|
|
128
|
-
create-project --name --companyId --contactPersonId --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate] [--description] [--requestQboAccountantNotification false]
|
|
130
|
+
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate <ISO> [--endDate <ISO>] [--description] [--requestQboAccountantNotification false]
|
|
129
131
|
update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
|
|
130
132
|
delete-project <id>
|
|
131
|
-
update-project-status <id> <status>
|
|
133
|
+
update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>]
|
|
134
|
+
Required when moving PITCH → WON: supply --projectManagerId or --projectManagerName.
|
|
132
135
|
status: ${projectStatusesForHelp.join(", ")}
|
|
133
136
|
|
|
134
137
|
Contacts
|
|
@@ -248,6 +251,14 @@ async function main() {
|
|
|
248
251
|
await uploadBudgetAttachmentFromPath(budgetId, filePath);
|
|
249
252
|
break;
|
|
250
253
|
}
|
|
254
|
+
case "mark-budget-won": {
|
|
255
|
+
const [budgetId, filePath] = positional;
|
|
256
|
+
if (!budgetId || !filePath) {
|
|
257
|
+
throw new Error("mark-budget-won requires <budgetId> <filePath>");
|
|
258
|
+
}
|
|
259
|
+
await markBudgetWonWithProof(budgetId, filePath);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
251
262
|
case "get-budget-items": {
|
|
252
263
|
const id = positional[0];
|
|
253
264
|
if (!id)
|
|
@@ -288,7 +299,40 @@ async function main() {
|
|
|
288
299
|
const id = positional[0];
|
|
289
300
|
if (!id)
|
|
290
301
|
throw new Error("create-budget-approval requires <budgetId>");
|
|
291
|
-
|
|
302
|
+
const companyPayloadRaw = getFlag(flags, "companyPayload");
|
|
303
|
+
const contactPayloadRaw = getFlag(flags, "contactPayload");
|
|
304
|
+
const eventPayloadRaw = getFlag(flags, "eventPayload");
|
|
305
|
+
await createBudgetApproval(id, {
|
|
306
|
+
company: companyPayloadRaw !== undefined
|
|
307
|
+
? parseJsonFlag(String(companyPayloadRaw), "--companyPayload")
|
|
308
|
+
: undefined,
|
|
309
|
+
contactPerson: contactPayloadRaw !== undefined
|
|
310
|
+
? parseJsonFlag(String(contactPayloadRaw), "--contactPayload")
|
|
311
|
+
: undefined,
|
|
312
|
+
eventDetails: eventPayloadRaw !== undefined
|
|
313
|
+
? parseJsonFlag(String(eventPayloadRaw), "--eventPayload")
|
|
314
|
+
: undefined,
|
|
315
|
+
});
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case "create-estimate": {
|
|
319
|
+
const id = positional[0];
|
|
320
|
+
if (!id)
|
|
321
|
+
throw new Error("create-estimate requires <budgetId>");
|
|
322
|
+
const companyPayloadRaw = getFlag(flags, "companyPayload");
|
|
323
|
+
const contactPayloadRaw = getFlag(flags, "contactPayload");
|
|
324
|
+
const eventPayloadRaw = getFlag(flags, "eventPayload");
|
|
325
|
+
await createEstimate(id, {
|
|
326
|
+
company: companyPayloadRaw !== undefined
|
|
327
|
+
? parseJsonFlag(String(companyPayloadRaw), "--companyPayload")
|
|
328
|
+
: undefined,
|
|
329
|
+
contactPerson: contactPayloadRaw !== undefined
|
|
330
|
+
? parseJsonFlag(String(contactPayloadRaw), "--contactPayload")
|
|
331
|
+
: undefined,
|
|
332
|
+
eventDetails: eventPayloadRaw !== undefined
|
|
333
|
+
? parseJsonFlag(String(eventPayloadRaw), "--eventPayload")
|
|
334
|
+
: undefined,
|
|
335
|
+
});
|
|
292
336
|
break;
|
|
293
337
|
}
|
|
294
338
|
case "create-budget": {
|
|
@@ -691,6 +735,10 @@ async function main() {
|
|
|
691
735
|
const name = getFlag(flags, "name");
|
|
692
736
|
const companyId = getFlag(flags, "companyId");
|
|
693
737
|
const contactPersonId = getFlag(flags, "contactPersonId");
|
|
738
|
+
const insideSalesId = getFlag(flags, "insideSalesId");
|
|
739
|
+
const businessDevelopmentId = getFlag(flags, "businessDevelopmentId");
|
|
740
|
+
const venue = getFlag(flags, "venue");
|
|
741
|
+
const pax = getFlag(flags, "pax");
|
|
694
742
|
const asanaTaskId = getFlag(flags, "asanaTaskId");
|
|
695
743
|
const slackChannelId = getFlag(flags, "slackChannelId");
|
|
696
744
|
const slackChannelUrl = getFlag(flags, "slackChannelUrl");
|
|
@@ -699,17 +747,25 @@ async function main() {
|
|
|
699
747
|
if (!name ||
|
|
700
748
|
!companyId ||
|
|
701
749
|
!contactPersonId ||
|
|
750
|
+
!insideSalesId ||
|
|
751
|
+
!businessDevelopmentId ||
|
|
752
|
+
!venue ||
|
|
753
|
+
!pax ||
|
|
702
754
|
!asanaTaskId ||
|
|
703
755
|
!slackChannelId ||
|
|
704
756
|
!slackChannelUrl ||
|
|
705
757
|
!slackChannelName ||
|
|
706
758
|
!startDate) {
|
|
707
|
-
throw new Error("create-project requires --name --companyId --contactPersonId --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO)");
|
|
759
|
+
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --pax --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName --startDate (ISO/datetime flags as appropriate); optional --endDate");
|
|
708
760
|
}
|
|
709
761
|
await createProject({
|
|
710
762
|
name,
|
|
711
763
|
companyId,
|
|
712
764
|
contactPersonId,
|
|
765
|
+
insideSalesId,
|
|
766
|
+
businessDevelopmentId,
|
|
767
|
+
venue,
|
|
768
|
+
pax,
|
|
713
769
|
asanaTaskId,
|
|
714
770
|
slackChannelId,
|
|
715
771
|
slackChannelUrl,
|
|
@@ -742,7 +798,12 @@ async function main() {
|
|
|
742
798
|
if (!id || !statusRaw) {
|
|
743
799
|
throw new Error("update-project-status requires <id> <status>");
|
|
744
800
|
}
|
|
745
|
-
|
|
801
|
+
const pmIdFlag = getFlag(flags, "projectManagerId");
|
|
802
|
+
const pmNameFlag = getFlag(flags, "projectManagerName");
|
|
803
|
+
await updateProjectStatus(id, parseProjectStatusForUpdate(statusRaw), {
|
|
804
|
+
...(pmIdFlag && { projectManagerId: String(pmIdFlag) }),
|
|
805
|
+
...(pmNameFlag && { projectManagerName: String(pmNameFlag) }),
|
|
806
|
+
});
|
|
746
807
|
break;
|
|
747
808
|
}
|
|
748
809
|
case "list-contacts": {
|
|
@@ -33,23 +33,15 @@ function requiredString(value, fieldLabel) {
|
|
|
33
33
|
}
|
|
34
34
|
return t;
|
|
35
35
|
}
|
|
36
|
-
/** Matches budget.createBudget —
|
|
36
|
+
/** Matches budget.createBudget — optional pipedriveDealId; Asana deal is project.asanaTaskId. */
|
|
37
37
|
export function parseCreateBudgetPayload(raw) {
|
|
38
38
|
const o = requireObject(raw, "create-budget payload");
|
|
39
39
|
return {
|
|
40
40
|
name: requiredString(o.name, "create-budget payload.name"),
|
|
41
|
-
date: toDate(o.date, "date"),
|
|
42
|
-
startTime: toDate(o.startTime, "startTime"),
|
|
43
|
-
endTime: toDate(o.endTime, "endTime"),
|
|
44
|
-
pax: requiredString(o.pax, "create-budget payload.pax"),
|
|
45
|
-
venue: requiredString(o.venue, "create-budget payload.venue"),
|
|
46
41
|
budget: requiredString(o.budget, "create-budget payload.budget"),
|
|
47
42
|
paymentTerm: requiredString(o.paymentTerm, "create-budget payload.paymentTerm"),
|
|
48
43
|
categoryId: requiredString(o.categoryId, "create-budget payload.categoryId"),
|
|
49
44
|
projectId: requiredString(o.projectId, "create-budget payload.projectId"),
|
|
50
|
-
insideSalesId: requiredString(o.insideSalesId, "create-budget payload.insideSalesId"),
|
|
51
|
-
businessDevelopmentId: requiredString(o.businessDevelopmentId, "create-budget payload.businessDevelopmentId"),
|
|
52
|
-
asanaTaskId: requiredString(o.asanaTaskId, "create-budget payload.asanaTaskId"),
|
|
53
45
|
sourceBudgetId: optionalString(o.sourceBudgetId),
|
|
54
46
|
pipedriveDealId: optionalString(o.pipedriveDealId),
|
|
55
47
|
};
|
|
@@ -200,6 +192,18 @@ export function parseUpdateProjectPayload(raw) {
|
|
|
200
192
|
else if (notification !== undefined && notification !== null) {
|
|
201
193
|
throw new Error("update-project payload.requestQboAccountantNotification must be a boolean when set.");
|
|
202
194
|
}
|
|
195
|
+
const slackChannelId = optionalString(o.slackChannelId);
|
|
196
|
+
const slackChannelUrl = optionalString(o.slackChannelUrl);
|
|
197
|
+
const slackChannelName = optionalString(o.slackChannelName);
|
|
198
|
+
const slackPatch = slackChannelId !== undefined &&
|
|
199
|
+
slackChannelUrl !== undefined &&
|
|
200
|
+
slackChannelName !== undefined
|
|
201
|
+
? {
|
|
202
|
+
slackChannelId,
|
|
203
|
+
slackChannelUrl,
|
|
204
|
+
slackChannelName,
|
|
205
|
+
}
|
|
206
|
+
: {};
|
|
203
207
|
return {
|
|
204
208
|
id: requiredString(o.id, "update-project payload.id"),
|
|
205
209
|
name: requiredString(o.name, "update-project payload.name"),
|
|
@@ -207,12 +211,17 @@ export function parseUpdateProjectPayload(raw) {
|
|
|
207
211
|
companyId: requiredString(o.companyId, "update-project payload.companyId"),
|
|
208
212
|
contactPersonId: requiredString(o.contactPersonId, "update-project payload.contactPersonId"),
|
|
209
213
|
asanaTaskId: requiredString(o.asanaTaskId, "update-project payload.asanaTaskId"),
|
|
214
|
+
insideSalesId: requiredString(o.insideSalesId, "update-project payload.insideSalesId"),
|
|
215
|
+
businessDevelopmentId: requiredString(o.businessDevelopmentId, "update-project payload.businessDevelopmentId"),
|
|
216
|
+
venue: requiredString(o.venue, "update-project payload.venue"),
|
|
217
|
+
pax: requiredString(o.pax, "update-project payload.pax"),
|
|
210
218
|
dateRange: {
|
|
211
219
|
from: toDate(range.from, "dateRange.from"),
|
|
212
220
|
to: range.to === undefined || range.to === null
|
|
213
221
|
? undefined
|
|
214
222
|
: toDate(range.to, "dateRange.to"),
|
|
215
223
|
},
|
|
224
|
+
...slackPatch,
|
|
216
225
|
...(requestQboAccountantNotification !== undefined && {
|
|
217
226
|
requestQboAccountantNotification,
|
|
218
227
|
}),
|
package/dist/prisma-enums.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Budget Builder CLI — list budgets, bills, approvals, suppliers; approve bills; change status. For AI agents (e.g. Chuck/OpenClaw).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"postpack": "node scripts/restore-package-json.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@trpc/client": "^11.
|
|
24
|
+
"@trpc/client": "^11.17.0",
|
|
25
25
|
"dotenv": "^17.4.2",
|
|
26
26
|
"mime-types": "^3.0.2",
|
|
27
27
|
"superjson": "^2.2.6"
|