@go-labs-sg/bb 1.2.9 → 1.4.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/dist/commands.js CHANGED
@@ -44,12 +44,66 @@ export async function getBudget(id) {
44
44
  out({ budget, items });
45
45
  }
46
46
  export async function updateBudgetStatus(budgetId, status) {
47
+ if (status === "ESTIMATE_ACCEPTED") {
48
+ throw new Error("Use mark-budget-won <budgetId> <filePath> so proof upload is included before setting ESTIMATE_ACCEPTED.");
49
+ }
47
50
  const result = await api.budget.updateBudgetStatus.mutate({
48
51
  id: budgetId,
49
52
  status,
50
53
  });
51
54
  out(result);
52
55
  }
56
+ const assertBudgetActionReadiness = async ({ budgetId, requireQuickbooksProject, actionLabel, }) => {
57
+ const budget = await api.budget.getBudget.query({ id: budgetId });
58
+ const hasNoItems = budget.budgetItems.length === 0;
59
+ if (hasNoItems) {
60
+ throw new Error(`Cannot ${actionLabel}: there are no line items in this budget.`);
61
+ }
62
+ const hasZeroCostItems = budget.budgetItems.some((item) => Number(item.cost ?? 0) === 0);
63
+ if (hasZeroCostItems) {
64
+ throw new Error(`Cannot ${actionLabel}: all line items must have a cost greater than zero.`);
65
+ }
66
+ const inactiveSuppliers = budget.budgetItems
67
+ .filter((item) => !item.supplier.active)
68
+ .map((item) => item.supplier.name);
69
+ const uniqueInactiveSuppliers = [...new Set(inactiveSuppliers)];
70
+ if (uniqueInactiveSuppliers.length > 0) {
71
+ throw new Error(`Cannot ${actionLabel}: inactive suppliers found (${uniqueInactiveSuppliers.join(", ")}).`);
72
+ }
73
+ if (requireQuickbooksProject && !budget.project.quickbooksProjectId) {
74
+ throw new Error("Cannot create estimate: project is not linked to QuickBooks yet.");
75
+ }
76
+ };
77
+ const runBudgetPreflight = async (budgetId, preflight) => {
78
+ if (!preflight)
79
+ return;
80
+ if (preflight.company) {
81
+ await api.company.updateCompany.mutate(preflight.company);
82
+ }
83
+ if (preflight.contactPerson) {
84
+ await api.contactPerson.updateContactPerson.mutate(preflight.contactPerson);
85
+ }
86
+ if (preflight.eventDetails) {
87
+ const budget = await api.budget.getBudget.query({ id: budgetId });
88
+ await api.budget.updateBudget.mutate({
89
+ id: budget.id,
90
+ name: budget.name,
91
+ date: new Date(preflight.eventDetails.date),
92
+ startTime: new Date(preflight.eventDetails.startTime),
93
+ endTime: new Date(preflight.eventDetails.endTime),
94
+ pax: String(budget.pax),
95
+ venue: budget.venue,
96
+ budget: String(budget.budget),
97
+ categoryId: budget.categoryId,
98
+ paymentTerm: budget.paymentTerm,
99
+ insideSalesId: budget.insideSalesId ?? budget.insideSales.id,
100
+ businessDevelopmentId: budget.businessDevelopmentId ?? budget.businessDevelopment.id,
101
+ pipedriveDealId: budget.pipedriveDealId ?? undefined,
102
+ asanaTaskId: budget.asanaTaskId ?? "",
103
+ projectId: budget.projectId,
104
+ });
105
+ }
106
+ };
53
107
  export async function listBills(opts) {
54
108
  const input = {
55
109
  projectId: opts.projectId,
@@ -264,6 +318,38 @@ export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
264
318
  });
265
319
  out(confirmed);
266
320
  }
321
+ export async function uploadBudgetWinProofFromPath(budgetId, filePath) {
322
+ const buf = await readFile(filePath);
323
+ const fileName = basename(filePath);
324
+ const size = buf.byteLength;
325
+ const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
326
+ budgetId,
327
+ fileName,
328
+ size,
329
+ });
330
+ const res = await fetch(uploadUrl, {
331
+ method: "PUT",
332
+ body: buf,
333
+ headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
334
+ });
335
+ if (!res.ok) {
336
+ throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
337
+ }
338
+ await api.attachment.confirmBudgetWinProofAttachment.mutate({
339
+ budgetId,
340
+ key,
341
+ name: fileName,
342
+ size,
343
+ });
344
+ const result = await api.budget.updateBudgetStatus.mutate({
345
+ id: budgetId,
346
+ status: "ESTIMATE_ACCEPTED",
347
+ });
348
+ out(result);
349
+ }
350
+ export async function markBudgetWonWithProof(budgetId, filePath) {
351
+ await uploadBudgetWinProofFromPath(budgetId, filePath);
352
+ }
267
353
  export async function getBillDetails(id) {
268
354
  const result = await api.bill.getById.query({ id });
269
355
  out(result);
@@ -297,7 +383,13 @@ export async function getBudgetVersions(budgetId) {
297
383
  });
298
384
  out(versions);
299
385
  }
300
- export async function createBudgetApproval(budgetId) {
386
+ export async function createBudgetApproval(budgetId, preflight) {
387
+ await runBudgetPreflight(budgetId, preflight);
388
+ await assertBudgetActionReadiness({
389
+ budgetId,
390
+ requireQuickbooksProject: false,
391
+ actionLabel: "request approval",
392
+ });
301
393
  const result = await api.budget.createBudgetApproval.mutate({ budgetId });
302
394
  const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
303
395
  let email;
@@ -322,6 +414,16 @@ export async function createBudgetApproval(budgetId) {
322
414
  email,
323
415
  });
324
416
  }
417
+ export async function createEstimate(budgetId, preflight) {
418
+ await runBudgetPreflight(budgetId, preflight);
419
+ await assertBudgetActionReadiness({
420
+ budgetId,
421
+ requireQuickbooksProject: true,
422
+ actionLabel: "create estimate",
423
+ });
424
+ const result = await api.quickbooks.createEstimate.mutate({ budgetId });
425
+ out(result);
426
+ }
325
427
  export async function getBudgetDetails(budgetId) {
326
428
  const detail = await api.budget.getBudgetDetail.query({ id: budgetId });
327
429
  out(detail);
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
+ mark-budget-won <budgetId> <filePath> Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED
79
80
  create-budget --payload '<json>' (budget.createBudget; ISO dates for date/startTime/endTime)
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>
@@ -248,6 +250,14 @@ async function main() {
248
250
  await uploadBudgetAttachmentFromPath(budgetId, filePath);
249
251
  break;
250
252
  }
253
+ case "mark-budget-won": {
254
+ const [budgetId, filePath] = positional;
255
+ if (!budgetId || !filePath) {
256
+ throw new Error("mark-budget-won requires <budgetId> <filePath>");
257
+ }
258
+ await markBudgetWonWithProof(budgetId, filePath);
259
+ break;
260
+ }
251
261
  case "get-budget-items": {
252
262
  const id = positional[0];
253
263
  if (!id)
@@ -288,7 +298,40 @@ async function main() {
288
298
  const id = positional[0];
289
299
  if (!id)
290
300
  throw new Error("create-budget-approval requires <budgetId>");
291
- await createBudgetApproval(id);
301
+ const companyPayloadRaw = getFlag(flags, "companyPayload");
302
+ const contactPayloadRaw = getFlag(flags, "contactPayload");
303
+ const eventPayloadRaw = getFlag(flags, "eventPayload");
304
+ await createBudgetApproval(id, {
305
+ company: companyPayloadRaw !== undefined
306
+ ? parseJsonFlag(String(companyPayloadRaw), "--companyPayload")
307
+ : undefined,
308
+ contactPerson: contactPayloadRaw !== undefined
309
+ ? parseJsonFlag(String(contactPayloadRaw), "--contactPayload")
310
+ : undefined,
311
+ eventDetails: eventPayloadRaw !== undefined
312
+ ? parseJsonFlag(String(eventPayloadRaw), "--eventPayload")
313
+ : undefined,
314
+ });
315
+ break;
316
+ }
317
+ case "create-estimate": {
318
+ const id = positional[0];
319
+ if (!id)
320
+ throw new Error("create-estimate requires <budgetId>");
321
+ const companyPayloadRaw = getFlag(flags, "companyPayload");
322
+ const contactPayloadRaw = getFlag(flags, "contactPayload");
323
+ const eventPayloadRaw = getFlag(flags, "eventPayload");
324
+ await createEstimate(id, {
325
+ company: companyPayloadRaw !== undefined
326
+ ? parseJsonFlag(String(companyPayloadRaw), "--companyPayload")
327
+ : undefined,
328
+ contactPerson: contactPayloadRaw !== undefined
329
+ ? parseJsonFlag(String(contactPayloadRaw), "--contactPayload")
330
+ : undefined,
331
+ eventDetails: eventPayloadRaw !== undefined
332
+ ? parseJsonFlag(String(eventPayloadRaw), "--eventPayload")
333
+ : undefined,
334
+ });
292
335
  break;
293
336
  }
294
337
  case "create-budget": {
@@ -113,8 +113,15 @@ export function parseCreateBillPayload(raw) {
113
113
  const vr = o.verificationResults;
114
114
  if (vr !== undefined && vr !== null) {
115
115
  const v = requireObject(vr, "verificationResults");
116
+ const verificationResultStatus = v.status;
117
+ if (verificationResultStatus !== "PENDING" &&
118
+ verificationResultStatus !== "PASS" &&
119
+ verificationResultStatus !== "FAIL" &&
120
+ verificationResultStatus !== "MANUAL_REVIEW") {
121
+ throw new Error("create-bill payload.verificationResults.status must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
122
+ }
116
123
  verificationResults = {
117
- status: v.status,
124
+ status: verificationResultStatus,
118
125
  extractedAmount: v.extractedAmount === null || v.extractedAmount === undefined
119
126
  ? null
120
127
  : Number(v.extractedAmount),
@@ -124,6 +131,9 @@ export function parseCreateBillPayload(raw) {
124
131
  confidence: v.confidence === null || v.confidence === undefined
125
132
  ? null
126
133
  : Number(v.confidence),
134
+ failureReason: v.failureReason === null || v.failureReason === undefined
135
+ ? null
136
+ : String(v.failureReason),
127
137
  };
128
138
  }
129
139
  const verificationStatus = o.verificationStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/bb",
3
- "version": "1.2.9",
3
+ "version": "1.4.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",