@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/README.md +14 -8
- package/dist/commands.js +385 -51
- package/dist/index.js +375 -24
- package/dist/parse-cli-enums.js +41 -3
- package/dist/parse-mutation-payload.js +52 -2
- package/dist/prisma-enums.js +16 -0
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
595
|
+
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
528
596
|
}
|
|
529
597
|
else {
|
|
530
598
|
try {
|
|
531
|
-
await api.email.sendBillApprovalRequestEmail.mutate(approvalIds);
|
|
532
|
-
email =
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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 =
|
|
937
|
+
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
820
938
|
}
|
|
821
939
|
else {
|
|
822
940
|
try {
|
|
823
|
-
await api.email.sendApprovalRequestEmail.mutate(approvalIds);
|
|
824
|
-
email =
|
|
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
|
|
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 =
|
|
1308
|
+
emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
1013
1309
|
}
|
|
1014
1310
|
else {
|
|
1015
1311
|
try {
|
|
1016
|
-
await api.email.sendSupplierApprovalRequestEmail.mutate(approvalIds);
|
|
1017
|
-
emailResult =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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?.
|
|
1493
|
-
|
|
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({
|