@go-labs-sg/bb 1.17.0 → 1.19.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
@@ -1,24 +1,53 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile } from "node:fs/promises";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
3
  import { basename } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { contentType } from "mime-types";
6
6
  import { api } from "./api-client.js";
7
7
  import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
8
8
  import { billStatusesForApi, } from "./parse-cli-enums.js";
9
- import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, } from "./parse-mutation-payload.js";
10
- import { BudgetStatus, } from "./prisma-enums.js";
9
+ import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseCreateCustomerInvoicePayload, parseCreateQuotationPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBillPaymentEvidencePayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, parseUpdateQuotationPayload, parseValidateBillSelectionPayload, } from "./parse-mutation-payload.js";
10
+ import { BudgetStatus, ProjectStatus, } from "./prisma-enums.js";
11
11
  import { createRichTextFromPlainText } from "./rich-text.js";
12
12
  const BUDGET = "BUDGET";
13
13
  const BILL = "BILL";
14
14
  const SUPPLIER = "SUPPLIER";
15
15
  const QUOTATION = "QUOTATION";
16
+ const CUSTOMER_INVOICE = "CUSTOMER_INVOICE";
16
17
  const ASANA_WON_LOST_SECTION_GIDS = new Set([
17
18
  "1211678338364908",
18
19
  "1211678338364907",
19
20
  ]);
20
21
  const MAX_ASANA_LEAD_CANDIDATES = 5;
21
22
  const ASANA_SEARCH_DETAIL_LIMIT = 15;
23
+ const NOTIFICATION_PAGE_LIMIT = 50;
24
+ const getAllNotifications = async () => {
25
+ const notifications = [];
26
+ let cursor;
27
+ do {
28
+ const page = await api.budget.getNotifications.query({
29
+ cursor,
30
+ limit: NOTIFICATION_PAGE_LIMIT,
31
+ });
32
+ notifications.push(...page.notifications);
33
+ cursor = page.nextCursor;
34
+ } while (cursor);
35
+ return notifications;
36
+ };
37
+ const createApprovalEmailDeliveryReport = ({ sentCount, skippedCount, message, }) => ({
38
+ sent: sentCount > 0,
39
+ count: sentCount,
40
+ sentCount,
41
+ skippedCount,
42
+ message,
43
+ });
44
+ const createApprovalEmailDeliveryFailure = (error) => ({
45
+ sent: false,
46
+ count: 0,
47
+ sentCount: 0,
48
+ skippedCount: 0,
49
+ error,
50
+ });
22
51
  function isPendingRequest(n) {
23
52
  return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
24
53
  }
@@ -212,6 +241,37 @@ const contentTypeHeaderForFileName = (fileName) => {
212
241
  const ct = contentType(fileName);
213
242
  return typeof ct === "string" ? ct : "application/octet-stream";
214
243
  };
244
+ const supportedAttachmentContentTypes = [
245
+ "application/pdf",
246
+ "image/gif",
247
+ "image/jpeg",
248
+ "image/png",
249
+ "image/webp",
250
+ ];
251
+ const attachmentContentTypeForFileName = (fileName) => {
252
+ const resolvedContentType = contentTypeHeaderForFileName(fileName);
253
+ const supportedContentType = supportedAttachmentContentTypes.find((contentTypeValue) => contentTypeValue === resolvedContentType);
254
+ if (supportedContentType)
255
+ return supportedContentType;
256
+ throw new Error(`Unsupported attachment type: ${fileName}`);
257
+ };
258
+ const billAttachmentContentTypeForFileName = (fileName) => {
259
+ const resolvedContentType = attachmentContentTypeForFileName(fileName);
260
+ if (resolvedContentType === "image/webp") {
261
+ throw new Error(`Unsupported QuickBooks bill attachment type: ${fileName}. Use PDF, GIF, JPEG, or PNG.`);
262
+ }
263
+ return resolvedContentType;
264
+ };
265
+ const QUOTATION_ATTACHMENT_MAX_SIZE = 20 * 1024 * 1024;
266
+ const quotationAttachmentContentTypeForFileName = (fileName) => {
267
+ const resolvedContentType = contentTypeHeaderForFileName(fileName);
268
+ if (resolvedContentType === "application/pdf" ||
269
+ resolvedContentType === "image/jpeg" ||
270
+ resolvedContentType === "image/png") {
271
+ return resolvedContentType;
272
+ }
273
+ throw new Error(`Unsupported quotation attachment type: ${fileName}. Use PDF, JPEG, or PNG.`);
274
+ };
215
275
  const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
216
276
  BudgetStatus.ESTIMATE_CREATED,
217
277
  BudgetStatus.ESTIMATE_SENT,
@@ -424,7 +484,7 @@ export async function approveBill(billId) {
424
484
  entity: `bill ${billId}`,
425
485
  details: "and send requester reply email",
426
486
  });
427
- const notifications = await api.budget.getNotifications.query();
487
+ const notifications = await getAllNotifications();
428
488
  const pending = notifications.filter((n) => isPendingRequest(n) &&
429
489
  n.type === BILL &&
430
490
  (n.billId === billId || n.bill?.id === billId));
@@ -436,6 +496,10 @@ export async function approveBill(billId) {
436
496
  billApprovalId: approval.id,
437
497
  status: "APPROVED",
438
498
  });
499
+ if (result.processing) {
500
+ out(result);
501
+ return;
502
+ }
439
503
  let email;
440
504
  try {
441
505
  await api.email.sendReplyBillApprovalEmail.mutate(result.updatedApproval);
@@ -453,7 +517,7 @@ export async function approveBill(billId) {
453
517
  });
454
518
  }
455
519
  export async function listApprovals(opts) {
456
- const notifications = await api.budget.getNotifications.query();
520
+ const notifications = await getAllNotifications();
457
521
  const pending = notifications.filter(isPendingRequest);
458
522
  const typeFilter = opts.type === undefined || opts.type === "ALL"
459
523
  ? (_n) => true
@@ -480,6 +544,10 @@ export async function listApprovals(opts) {
480
544
  id = n.supplierQuotation.id;
481
545
  name = `Quotation ${n.supplierQuotation.supplier.name}`;
482
546
  }
547
+ else if (n.type === CUSTOMER_INVOICE && n.customerInvoiceBatch) {
548
+ id = n.customerInvoiceBatch.id;
549
+ name = `Customer invoice ${n.customerInvoiceBatch.project.name}`;
550
+ }
483
551
  else {
484
552
  id = n.id;
485
553
  name = "Unknown";
@@ -524,19 +592,15 @@ export async function createBillApproval(billId) {
524
592
  const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
525
593
  let email;
526
594
  if (approvalIds.length === 0) {
527
- email = { sent: false, count: 0, error: "No approval ids returned" };
595
+ email = createApprovalEmailDeliveryFailure("No approval ids returned");
528
596
  }
529
597
  else {
530
598
  try {
531
- await api.email.sendBillApprovalRequestEmail.mutate(approvalIds);
532
- email = { sent: true, count: approvalIds.length };
599
+ const delivery = await api.email.sendBillApprovalRequestEmail.mutate(approvalIds);
600
+ email = createApprovalEmailDeliveryReport(delivery);
533
601
  }
534
602
  catch (error) {
535
- email = {
536
- sent: false,
537
- count: approvalIds.length,
538
- error: error instanceof Error ? error.message : String(error),
539
- };
603
+ email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
540
604
  }
541
605
  }
542
606
  out({
@@ -589,6 +653,43 @@ export async function getBillAttachments(id) {
589
653
  const result = await api.bill.getAttachments.query({ id });
590
654
  out(result);
591
655
  }
656
+ export async function stageBillAttachmentsFromPaths(projectId, filePaths) {
657
+ const attachments = [];
658
+ for (const filePath of filePaths) {
659
+ const buffer = await readFile(filePath);
660
+ const fileName = basename(filePath);
661
+ const size = buffer.byteLength;
662
+ const contentType = billAttachmentContentTypeForFileName(fileName);
663
+ const { uploadUrl, key } = await api.attachment.requestStagedBillAttachmentUpload.mutate({
664
+ projectId,
665
+ fileName,
666
+ size,
667
+ contentType,
668
+ });
669
+ const response = await fetch(uploadUrl, {
670
+ method: "PUT",
671
+ body: buffer,
672
+ headers: { "Content-Type": contentType },
673
+ });
674
+ if (!response.ok) {
675
+ throw new Error(`S3 upload failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
676
+ }
677
+ attachments.push({ id: randomUUID(), key, name: fileName, size });
678
+ }
679
+ out({
680
+ success: true,
681
+ projectId,
682
+ count: attachments.length,
683
+ attachments,
684
+ });
685
+ }
686
+ export const cleanupStagedBillAttachments = async (projectId, keys) => {
687
+ const result = await api.attachment.deleteStagedBillAttachments.mutate({
688
+ projectId,
689
+ keys,
690
+ });
691
+ out(result);
692
+ };
592
693
  export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
593
694
  const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
594
695
  out(confirmed);
@@ -597,15 +698,17 @@ const uploadSingleBillAttachmentFromPath = async (billId, filePath, type = "BILL
597
698
  const buf = await readFile(filePath);
598
699
  const fileName = basename(filePath);
599
700
  const size = buf.byteLength;
701
+ const attachmentContentType = billAttachmentContentTypeForFileName(fileName);
600
702
  const { uploadUrl, key } = await api.attachment.requestBillAttachmentUpload.mutate({
601
703
  billId,
704
+ contentType: attachmentContentType,
602
705
  fileName,
603
706
  size,
604
707
  });
605
708
  const res = await fetch(uploadUrl, {
606
709
  method: "PUT",
607
710
  body: buf,
608
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
711
+ headers: { "Content-Type": attachmentContentType },
609
712
  });
610
713
  if (!res.ok) {
611
714
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -659,6 +762,10 @@ export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [],
659
762
  export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
660
763
  const buf = await readFile(filePath);
661
764
  const fileName = basename(filePath);
765
+ if (buf.byteLength > QUOTATION_ATTACHMENT_MAX_SIZE) {
766
+ throw new Error("Quotation attachment must be 20MB or smaller.");
767
+ }
768
+ const attachmentContentType = quotationAttachmentContentTypeForFileName(fileName);
662
769
  const ext = fileName
663
770
  .split(".")
664
771
  .pop()
@@ -671,7 +778,7 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
671
778
  const res = await fetch(uploadUrl, {
672
779
  method: "PUT",
673
780
  body: buf,
674
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
781
+ headers: { "Content-Type": attachmentContentType },
675
782
  });
676
783
  if (!res.ok) {
677
784
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -683,20 +790,29 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
683
790
  size: buf.byteLength,
684
791
  });
685
792
  }
793
+ export const cleanupStagedQuotationAttachments = async (projectId, keys) => {
794
+ const result = await api.quotation.deleteStagedAttachments.mutate({
795
+ projectId,
796
+ keys,
797
+ });
798
+ out(result);
799
+ };
686
800
  export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
687
801
  await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
688
802
  const buf = await readFile(filePath);
689
803
  const fileName = basename(filePath);
690
804
  const size = buf.byteLength;
805
+ const attachmentContentType = attachmentContentTypeForFileName(fileName);
691
806
  const { uploadUrl, key } = await api.attachment.requestBudgetAttachmentUpload.mutate({
692
807
  budgetId,
808
+ contentType: attachmentContentType,
693
809
  fileName,
694
810
  size,
695
811
  });
696
812
  const res = await fetch(uploadUrl, {
697
813
  method: "PUT",
698
814
  body: buf,
699
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
815
+ headers: { "Content-Type": attachmentContentType },
700
816
  });
701
817
  if (!res.ok) {
702
818
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -719,15 +835,17 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
719
835
  const buf = await readFile(filePath);
720
836
  const fileName = basename(filePath);
721
837
  const size = buf.byteLength;
838
+ const attachmentContentType = attachmentContentTypeForFileName(fileName);
722
839
  const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
723
840
  budgetId,
841
+ contentType: attachmentContentType,
724
842
  fileName,
725
843
  size,
726
844
  });
727
845
  const res = await fetch(uploadUrl, {
728
846
  method: "PUT",
729
847
  body: buf,
730
- headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
848
+ headers: { "Content-Type": attachmentContentType },
731
849
  });
732
850
  if (!res.ok) {
733
851
  throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
@@ -816,19 +934,15 @@ export async function createBudgetApproval(budgetId, preflight) {
816
934
  const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
817
935
  let email;
818
936
  if (approvalIds.length === 0) {
819
- email = { sent: false, count: 0, error: "No approval ids returned" };
937
+ email = createApprovalEmailDeliveryFailure("No approval ids returned");
820
938
  }
821
939
  else {
822
940
  try {
823
- await api.email.sendApprovalRequestEmail.mutate(approvalIds);
824
- email = { sent: true, count: approvalIds.length };
941
+ const delivery = await api.email.sendApprovalRequestEmail.mutate(approvalIds);
942
+ email = createApprovalEmailDeliveryReport(delivery);
825
943
  }
826
944
  catch (error) {
827
- email = {
828
- sent: false,
829
- count: approvalIds.length,
830
- error: error instanceof Error ? error.message : String(error),
831
- };
945
+ email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
832
946
  }
833
947
  }
834
948
  out({
@@ -909,6 +1023,68 @@ export async function updateBudgetDiscountFromPayload(raw) {
909
1023
  const result = await api.budget.updateBudgetDiscount.mutate(input);
910
1024
  out(result);
911
1025
  }
1026
+ export const deleteBudgetDiscount = async (budgetId) => {
1027
+ await assertSensitiveWorkflowConfirmed({
1028
+ action: "Remove budget discount",
1029
+ entity: `budget ${budgetId}`,
1030
+ details: "and return the budget to DRAFT",
1031
+ });
1032
+ const result = await api.budget.deleteBudgetDiscount.mutate({ budgetId });
1033
+ out(result);
1034
+ };
1035
+ export const deleteBudgetCommission = async (budgetId) => {
1036
+ await assertSensitiveWorkflowConfirmed({
1037
+ action: "Remove budget commission",
1038
+ entity: `budget ${budgetId}`,
1039
+ details: "and return the budget to DRAFT",
1040
+ });
1041
+ const result = await api.budget.deleteBudgetCommission.mutate({ budgetId });
1042
+ out(result);
1043
+ };
1044
+ export const setBudgetItemsNotUtilized = async (budgetItemIds, notUtilized) => {
1045
+ await assertSensitiveWorkflowConfirmed({
1046
+ action: notUtilized
1047
+ ? "Mark budget lines Not Utilized"
1048
+ : "Restore budget line",
1049
+ entity: `budget item${budgetItemIds.length === 1 ? "" : "s"} ${budgetItemIds.join(", ")}`,
1050
+ details: notUtilized
1051
+ ? "and remove linked QuickBooks placeholder expenses"
1052
+ : "and restore its QuickBooks placeholder expense when required",
1053
+ });
1054
+ const result = await api.budget.setBudgetItemsNotUtilized.mutate({
1055
+ budgetItemIds,
1056
+ notUtilized,
1057
+ });
1058
+ out(result);
1059
+ };
1060
+ export const createPlaceholderBillForBudgetItem = async (budgetItemId) => {
1061
+ await assertSensitiveWorkflowConfirmed({
1062
+ action: "Create QuickBooks placeholder bill",
1063
+ entity: `budget item ${budgetItemId}`,
1064
+ });
1065
+ const result = await api.budget.createPlaceholderBillForBudgetItem.mutate({
1066
+ budgetItemId,
1067
+ });
1068
+ out(result);
1069
+ };
1070
+ export const renameBudgetVersion = async (versionId, newName) => {
1071
+ const result = await api.budgetVersion.renameBudgetVersion.mutate({
1072
+ versionId,
1073
+ newName,
1074
+ });
1075
+ out(result);
1076
+ };
1077
+ export const restoreBudgetVersion = async (versionId) => {
1078
+ await assertSensitiveWorkflowConfirmed({
1079
+ action: "Restore budget version",
1080
+ entity: `budget version ${versionId}`,
1081
+ details: "replace the current budget, project fields, items, and attachments, clear its QuickBooks estimate link, and return it to DRAFT",
1082
+ });
1083
+ const result = await api.budgetVersion.restoreBudgetVersion.mutate({
1084
+ versionId,
1085
+ });
1086
+ out(result);
1087
+ };
912
1088
  export async function deleteItemById(itemId) {
913
1089
  const result = await api.item.deleteItem.mutate({ id: itemId });
914
1090
  out(result);
@@ -963,9 +1139,19 @@ export async function getQuotationDetails(id) {
963
1139
  out(result);
964
1140
  }
965
1141
  export async function createQuotationFromPayload(raw) {
966
- const result = await api.quotation.createDraft.mutate(raw);
1142
+ const input = parseCreateQuotationPayload(raw);
1143
+ const result = await api.quotation.createDraft.mutate(input);
967
1144
  out(result);
968
1145
  }
1146
+ export const updateQuotationFromPayload = async (raw) => {
1147
+ const input = parseUpdateQuotationPayload(raw);
1148
+ const result = await api.quotation.updateDraftOrRejected.mutate(input);
1149
+ out(result);
1150
+ };
1151
+ export const deleteQuotationById = async (id) => {
1152
+ const result = await api.quotation.delete.mutate({ id });
1153
+ out(result);
1154
+ };
969
1155
  export async function submitQuotation(id) {
970
1156
  await assertSensitiveWorkflowConfirmed({
971
1157
  action: "Submit quotation for approval",
@@ -981,11 +1167,121 @@ export async function downloadQuotationPdf(id, fileRole) {
981
1167
  });
982
1168
  out(result);
983
1169
  }
1170
+ export const checkCustomerInvoiceReadiness = async (budgetId) => {
1171
+ const result = await api.customerInvoice.getInvoiceReadiness.query({
1172
+ budgetId,
1173
+ });
1174
+ out(result);
1175
+ };
1176
+ export const listCustomerInvoices = async (input) => {
1177
+ const result = await api.customerInvoice.listByBudget.query(input);
1178
+ out(result);
1179
+ };
1180
+ export const getCustomerInvoice = async (batchId) => {
1181
+ const result = await api.customerInvoice.getInvoiceDetail.query({ batchId });
1182
+ out(result);
1183
+ };
1184
+ export const createCustomerInvoice = async (raw) => {
1185
+ const input = parseCreateCustomerInvoicePayload(raw);
1186
+ await assertSensitiveWorkflowConfirmed({
1187
+ action: "Create customer invoice",
1188
+ entity: `budget ${input.budgetId}`,
1189
+ details: "create the invoice in QuickBooks and either approve it as admin or send approval-request emails",
1190
+ });
1191
+ const result = await api.customerInvoice.createInvoiceBatch.mutate(input);
1192
+ out(result);
1193
+ };
1194
+ export const discardCreatingCustomerInvoice = async (batchId) => {
1195
+ await assertSensitiveWorkflowConfirmed({
1196
+ action: "Discard unfinished customer invoice",
1197
+ entity: `invoice batch ${batchId}`,
1198
+ });
1199
+ const result = await api.customerInvoice.discardCreatingInvoiceBatch.mutate({
1200
+ batchId,
1201
+ });
1202
+ out(result);
1203
+ };
1204
+ export const deleteCustomerInvoice = async (batchId) => {
1205
+ await assertSensitiveWorkflowConfirmed({
1206
+ action: "Delete customer invoice",
1207
+ entity: `invoice batch ${batchId}`,
1208
+ details: "delete its invoices from QuickBooks and remove the local batch",
1209
+ });
1210
+ const result = await api.customerInvoice.deleteInvoiceBatch.mutate({
1211
+ batchId,
1212
+ });
1213
+ out(result);
1214
+ };
1215
+ export const voidCustomerInvoice = async (batchId) => {
1216
+ await assertSensitiveWorkflowConfirmed({
1217
+ action: "Void customer invoice",
1218
+ entity: `invoice batch ${batchId}`,
1219
+ details: "void its invoices in QuickBooks",
1220
+ });
1221
+ const result = await api.customerInvoice.voidInvoiceBatch.mutate({ batchId });
1222
+ out(result);
1223
+ };
1224
+ export const approveCustomerInvoice = async (batchId) => {
1225
+ await assertSensitiveWorkflowConfirmed({
1226
+ action: "Approve customer invoice",
1227
+ entity: `invoice batch ${batchId}`,
1228
+ details: "and notify its creator",
1229
+ });
1230
+ const result = await api.customerInvoice.approveInvoiceBatch.mutate({
1231
+ batchId,
1232
+ });
1233
+ out(result);
1234
+ };
1235
+ export const rejectCustomerInvoice = async (batchId, rejectionReason) => {
1236
+ await assertSensitiveWorkflowConfirmed({
1237
+ action: "Reject customer invoice",
1238
+ entity: `invoice batch ${batchId}`,
1239
+ details: `with reason "${rejectionReason}", void it in QuickBooks, and notify its creator`,
1240
+ });
1241
+ const result = await api.customerInvoice.rejectInvoiceBatch.mutate({
1242
+ batchId,
1243
+ rejectionReason,
1244
+ });
1245
+ out(result);
1246
+ };
1247
+ export const downloadCustomerInvoicePdf = async (invoiceId, outputPath) => {
1248
+ const result = await api.customerInvoice.downloadPdf.mutate({ invoiceId });
1249
+ const resolvedOutputPath = outputPath?.trim() || result.fileName;
1250
+ const contents = Buffer.from(result.base64, "base64");
1251
+ await writeFile(resolvedOutputPath, contents);
1252
+ out({
1253
+ contentType: result.contentType,
1254
+ fileName: result.fileName,
1255
+ outputPath: resolvedOutputPath,
1256
+ size: contents.byteLength,
1257
+ });
1258
+ };
1259
+ export const syncCustomerInvoice = async (invoiceId) => {
1260
+ await assertSensitiveWorkflowConfirmed({
1261
+ action: "Sync customer invoice from QuickBooks",
1262
+ entity: `invoice ${invoiceId}`,
1263
+ details: "and update its local status, QuickBooks metadata, balance, and history",
1264
+ });
1265
+ const result = await api.customerInvoice.syncInvoiceStatus.mutate({
1266
+ invoiceId,
1267
+ });
1268
+ out(result);
1269
+ };
984
1270
  export async function updateBillFromPayload(raw) {
985
1271
  const input = parseUpdateBillPayload(raw);
986
1272
  const result = await api.bill.update.mutate(input);
987
1273
  out(result);
988
1274
  }
1275
+ export const validateBillSelectionFromPayload = async (raw) => {
1276
+ const input = parseValidateBillSelectionPayload(raw);
1277
+ const result = await api.bill.validateSelection.mutate(input);
1278
+ out(result);
1279
+ };
1280
+ export const updateBillPaymentEvidenceFromPayload = async (raw) => {
1281
+ const input = parseUpdateBillPaymentEvidencePayload(raw);
1282
+ const result = await api.bill.updatePaymentEvidence.mutate(input);
1283
+ out(result);
1284
+ };
989
1285
  export async function deleteBillById(id) {
990
1286
  const result = await api.bill.delete.mutate({ id });
991
1287
  out(result);
@@ -1009,19 +1305,15 @@ export async function createSupplierFromPayload(raw) {
1009
1305
  let emailResult;
1010
1306
  const approvalIds = (approval.results ?? []).map((item) => ({ id: item.id }));
1011
1307
  if (approvalIds.length === 0) {
1012
- emailResult = { sent: false, count: 0, error: "No approval ids returned" };
1308
+ emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
1013
1309
  }
1014
1310
  else {
1015
1311
  try {
1016
- await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1017
- emailResult = { sent: true, count: approvalIds.length };
1312
+ const delivery = await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1313
+ emailResult = createApprovalEmailDeliveryReport(delivery);
1018
1314
  }
1019
1315
  catch (error) {
1020
- emailResult = {
1021
- sent: false,
1022
- count: approvalIds.length,
1023
- error: error instanceof Error ? error.message : String(error),
1024
- };
1316
+ emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
1025
1317
  }
1026
1318
  }
1027
1319
  out({
@@ -1058,27 +1350,23 @@ export async function updateSupplierFromPayload(raw) {
1058
1350
  approvalResult = {
1059
1351
  requested: true,
1060
1352
  approvalCount: 0,
1061
- email: { sent: false, count: 0, error: "No approval ids returned" },
1353
+ email: createApprovalEmailDeliveryFailure("No approval ids returned"),
1062
1354
  };
1063
1355
  }
1064
1356
  else {
1065
1357
  try {
1066
- await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1358
+ const delivery = await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
1067
1359
  approvalResult = {
1068
1360
  requested: true,
1069
1361
  approvalCount: approvalIds.length,
1070
- email: { sent: true, count: approvalIds.length },
1362
+ email: createApprovalEmailDeliveryReport(delivery),
1071
1363
  };
1072
1364
  }
1073
1365
  catch (error) {
1074
1366
  approvalResult = {
1075
1367
  requested: true,
1076
1368
  approvalCount: approvalIds.length,
1077
- email: {
1078
- sent: false,
1079
- count: approvalIds.length,
1080
- error: error instanceof Error ? error.message : String(error),
1081
- },
1369
+ email: createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error)),
1082
1370
  };
1083
1371
  }
1084
1372
  }
@@ -1131,7 +1419,7 @@ export async function approveBudget(budgetId) {
1131
1419
  entity: `budget ${budgetId}`,
1132
1420
  details: "and send requester reply email",
1133
1421
  });
1134
- const notifications = await api.budget.getNotifications.query();
1422
+ const notifications = await getAllNotifications();
1135
1423
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1136
1424
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1137
1425
  n.type === ExtendedApprovalType.BUDGET &&
@@ -1167,7 +1455,7 @@ export async function rejectBudget(budgetId, reason) {
1167
1455
  entity: `budget ${budgetId}`,
1168
1456
  details: `with reason "${reason}" and send requester reply email`,
1169
1457
  });
1170
- const notifications = await api.budget.getNotifications.query();
1458
+ const notifications = await getAllNotifications();
1171
1459
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1172
1460
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1173
1461
  n.type === ExtendedApprovalType.BUDGET &&
@@ -1265,7 +1553,7 @@ export async function approveSupplier(supplierId) {
1265
1553
  entity: `supplier ${supplierId}`,
1266
1554
  details: "and send requester reply email",
1267
1555
  });
1268
- const notifications = await api.budget.getNotifications.query();
1556
+ const notifications = await getAllNotifications();
1269
1557
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1270
1558
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1271
1559
  n.type === ExtendedApprovalType.SUPPLIER &&
@@ -1303,7 +1591,7 @@ export async function rejectSupplier(supplierId, reason) {
1303
1591
  entity: `supplier ${supplierId}`,
1304
1592
  details: `with reason "${reason}" and send requester reply email`,
1305
1593
  });
1306
- const notifications = await api.budget.getNotifications.query();
1594
+ const notifications = await getAllNotifications();
1307
1595
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1308
1596
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1309
1597
  n.type === ExtendedApprovalType.SUPPLIER &&
@@ -1342,7 +1630,7 @@ export async function rejectBill(billId, reason) {
1342
1630
  entity: `bill ${billId}`,
1343
1631
  details: `with reason "${reason}" and send requester reply email`,
1344
1632
  });
1345
- const notifications = await api.budget.getNotifications.query();
1633
+ const notifications = await getAllNotifications();
1346
1634
  const pending = notifications.filter((n) => n.notificationType === "request" &&
1347
1635
  n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
1348
1636
  n.type === ExtendedApprovalType.BILL &&
@@ -1356,6 +1644,10 @@ export async function rejectBill(billId, reason) {
1356
1644
  status: ExtendedApprovalStatus.REJECTED,
1357
1645
  rejectionReason: reason,
1358
1646
  });
1647
+ if (result.processing) {
1648
+ out(result);
1649
+ return;
1650
+ }
1359
1651
  let email;
1360
1652
  try {
1361
1653
  await api.email.sendReplyBillApprovalEmail.mutate(result.updatedApproval);
@@ -1377,7 +1669,7 @@ export async function approveQuotation(quotationId) {
1377
1669
  action: "Approve quotation",
1378
1670
  entity: `quotation ${quotationId}`,
1379
1671
  });
1380
- const notifications = await api.budget.getNotifications.query();
1672
+ const notifications = await getAllNotifications();
1381
1673
  const pending = notifications.filter((n) => isPendingRequest(n) &&
1382
1674
  n.type === QUOTATION &&
1383
1675
  (n.supplierQuotationId === quotationId ||
@@ -1397,7 +1689,7 @@ export async function rejectQuotation(quotationId, reason) {
1397
1689
  entity: `quotation ${quotationId}`,
1398
1690
  details: `with reason "${reason}"`,
1399
1691
  });
1400
- const notifications = await api.budget.getNotifications.query();
1692
+ const notifications = await getAllNotifications();
1401
1693
  const pending = notifications.filter((n) => isPendingRequest(n) &&
1402
1694
  n.type === QUOTATION &&
1403
1695
  (n.supplierQuotationId === quotationId ||
@@ -1466,6 +1758,7 @@ export async function createProject(opts) {
1466
1758
  contactPersonId: opts.contactPersonId,
1467
1759
  insideSalesId: opts.insideSalesId,
1468
1760
  businessDevelopmentId: opts.businessDevelopmentId,
1761
+ projectManagerId: opts.projectManagerId,
1469
1762
  venue: opts.venue,
1470
1763
  pax: opts.pax,
1471
1764
  asanaTaskId: links.asanaTaskId,
@@ -1489,8 +1782,8 @@ export async function updateProjectStatus(id, status, opts) {
1489
1782
  id,
1490
1783
  status,
1491
1784
  ...(opts?.projectManagerId && { projectManagerId: opts.projectManagerId }),
1492
- ...(opts?.projectManagerName && {
1493
- projectManagerName: opts.projectManagerName,
1785
+ ...(opts?.projectManagerEmail && {
1786
+ projectManagerEmail: opts.projectManagerEmail,
1494
1787
  }),
1495
1788
  ...(opts?.wonOverrideReason && {
1496
1789
  wonOverrideReason: opts.wonOverrideReason,
@@ -1498,6 +1791,47 @@ export async function updateProjectStatus(id, status, opts) {
1498
1791
  });
1499
1792
  out(result);
1500
1793
  }
1794
+ export const checkProjectReconciliation = async (projectId) => {
1795
+ const result = await api.project.getReconciliationReview.query({ projectId });
1796
+ out(result);
1797
+ };
1798
+ export const reconcileProject = async (projectId) => {
1799
+ await assertSensitiveWorkflowConfirmed({
1800
+ action: "Reconcile project",
1801
+ entity: `project ${projectId}`,
1802
+ details: "rerun all reconciliation checks and mark it RECONCILED",
1803
+ });
1804
+ const result = await api.project.reconcile.mutate({ projectId });
1805
+ out(result);
1806
+ };
1807
+ export const completeProject = async (projectId) => {
1808
+ await updateProjectStatus(projectId, ProjectStatus.COMPLETED);
1809
+ };
1810
+ export const importQuickBooksProjectId = async (projectId, projectUrl) => {
1811
+ const result = await api.project.importQboProjectId.mutate({
1812
+ projectId,
1813
+ projectUrl,
1814
+ });
1815
+ out(result);
1816
+ };
1817
+ export const listIntegrationOperations = async (input) => {
1818
+ const result = await api.integration.listOperations.query(input);
1819
+ out(result);
1820
+ };
1821
+ export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled) => {
1822
+ await assertSensitiveWorkflowConfirmed({
1823
+ action: "Retry integration operation",
1824
+ entity: `operation ${operationId}`,
1825
+ details: confirmExternalStateReconciled
1826
+ ? "after confirming the external state has been reconciled"
1827
+ : undefined,
1828
+ });
1829
+ const result = await api.integration.retryOperation.mutate({
1830
+ operationId,
1831
+ confirmExternalStateReconciled,
1832
+ });
1833
+ out(result);
1834
+ };
1501
1835
  // --- Contacts ---
1502
1836
  export async function listContacts(companyId) {
1503
1837
  const contacts = await api.contactPerson.getContactPersonByCompanyId.query({