@go-labs-sg/bb 1.18.1 → 1.20.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 +13 -7
- package/dist/api-client.js +3 -3
- package/dist/cli-trace.js +30 -0
- package/dist/commands.js +373 -13
- package/dist/index.js +358 -18
- package/dist/parse-cli-enums.js +41 -3
- package/dist/parse-mutation-payload.js +62 -0
- package/dist/prisma-enums.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ For AI agents: bills and claims are stored as the same underlying bill records.
|
|
|
6
6
|
|
|
7
7
|
For AI agents using this CLI: always check whether a newer `@go-labs-sg/bb` version is available before running Budget Builder commands, then update the CLI if needed so command flags and API behavior stay current.
|
|
8
8
|
|
|
9
|
-
For AI agents using this CLI: do not execute approval decisions, approval requests, status updates, `mark-budget-won`, `submit-quotation`, or supplier approval request side effects until the user explicitly confirms the exact entity, target state, and side effects. Inspect the entity first, summarize what will change and whether emails/integrations will run, then wait for a clear confirmation from the user. These sensitive workflow commands also require an interactive `CONFIRM` prompt at runtime and abort in non-interactive shells.
|
|
9
|
+
For AI agents using this CLI: do not execute approval decisions, approval requests, status updates, estimate emails, customer-invoice changes, integration retries, `mark-budget-won`, `submit-quotation`, or supplier approval request side effects until the user explicitly confirms the exact entity, target state, and side effects. Inspect the entity first, summarize what will change and whether emails/integrations will run, then wait for a clear confirmation from the user. These sensitive workflow commands also require an interactive `CONFIRM` prompt at runtime and abort in non-interactive shells.
|
|
10
10
|
|
|
11
11
|
**Registry:** [`@go-labs-sg/bb`](https://www.npmjs.com/package/@go-labs-sg/bb)
|
|
12
12
|
|
|
@@ -72,7 +72,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
72
72
|
|
|
73
73
|
**Budget status automation:** Setting a budget to `ESTIMATE_ACCEPTED` requires a confirmed win-proof attachment. If the parent project is `PITCH` or `LOST`, the API now marks it `WON` automatically and, when the `asana-tasks` feature flag is enabled, creates an Asana Event Ops section from Prompt 5 tasks. Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date. When rejecting the only accepted/closed budget on a commercial project, pass `--projectStatusOnCommercialRejection PITCH|LOST`.
|
|
74
74
|
|
|
75
|
-
**Sensitive workflow changes:** Approval decisions/requests, entity status updates, `mark-budget-won`, `submit-quotation`, and supplier approval request side effects require an interactive `CONFIRM` prompt. Non-interactive runs abort before the guarded workflow mutation. Agents must get user confirmation in chat before attempting the command; the prompt is a final runtime guard, not a replacement for user approval.
|
|
75
|
+
**Sensitive workflow changes:** Approval decisions/requests, entity status updates, estimate emails, customer-invoice changes, integration retries, `mark-budget-won`, `submit-quotation`, and supplier approval request side effects require an interactive `CONFIRM` prompt. Non-interactive runs abort before the guarded workflow mutation. Agents must get user confirmation in chat before attempting the command; the prompt is a final runtime guard, not a replacement for user approval.
|
|
76
76
|
|
|
77
77
|
**Approval email exclusions:** Approval requests create pending database records for every eligible approver, including configured non-recipient admin accounts. Automated approval-request emails skip those accounts.
|
|
78
78
|
|
|
@@ -80,21 +80,27 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
80
80
|
|
|
81
81
|
**Mutations with `--payload`:** Commands such as `create-budget`, `create-bill`, `update-supplier`, etc. take a single JSON object (`--payload '<json>'`) matching the corresponding tRPC procedure input. Use ISO strings for date/datetime fields; the CLI coerces them where needed. The API still validates the full shape. Plain `description` fields for item create/update are converted to `descriptionRichText`; pass `descriptionRichText` directly when formatted Tiptap JSON is required. For **`update-project`**, the project window is `dateRange.from` and `dateRange.to` (optional end); there are no separate event-date fields on the project payload. **`create-budget` / `update-budget`** do not accept `asanaTaskId`; configure the deal card on the project (`update-project` / project settings).
|
|
82
82
|
|
|
83
|
+
**Contact-person estimate email:** `send-estimate-to-contact-person --payload '<json>'` calls the same `email.sendEstimateToContactPerson` procedure as the web composer. The payload requires `budgetId`, `estimateId`, optional `estimateDocNumber`, `to`, `cc`, `replyTo`, `subject`, HTML `content`, and HTML `signature`. It sends the raw PDF returned by QuickBooks together with the standard terms and Budget Builder budget attachments, then marks the budget `ESTIMATE_SENT`. The command requires interactive `CONFIRM`; inspect the budget, recipients, and HTML first, and do not retry blindly after an ambiguous delivery failure.
|
|
84
|
+
|
|
83
85
|
### Command overview
|
|
84
86
|
|
|
85
87
|
| Area | Commands (non-exhaustive) |
|
|
86
88
|
| --- | --- |
|
|
87
|
-
| **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
88
|
-
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (
|
|
89
|
-
| **Quotations** | `list-quotations
|
|
90
|
-
| **
|
|
91
|
-
| **
|
|
89
|
+
| **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`, `rename-budget-version`, `restore-budget-version`, `update-budget-status` (`ESTIMATE_ACCEPTED` requires win proof, auto-marks `PITCH` or `LOST` projects `WON`, and auto-creates an Asana Event Ops section from Prompt 5 tasks when enabled; Prompt 5 skips quantity-zero items, combines matching non-GO-internal suppliers, keeps each GO internal item separate, removes generated work that repeats another line item or group, and schedules applicable subtasks relative to the project start date; `--projectStatusOnCommercialRejection PITCH\|LOST` is required when rejecting the only accepted/closed budget on a commercial project), `mark-budget-won` (`<budgetId>` + proof file path; `PITCH` or `LOST` projects become `WON` automatically), `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval` (also sends approval request emails), `create-estimate`, `send-estimate-to-contact-person` (same contact-person email workflow as web), `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `mark-budget-items-not-utilized`, `restore-budget-item`, `create-placeholder-bill` (admin recovery), `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `delete-budget-commission`, `update-budget-discount`, `delete-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
90
|
+
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `validate-bill-selection` (`--payload`; runs the same supplier, line-item, and quotation-coverage checks as the web flow), `stage-bill-attachment` (securely uploads invoice/payment-proof files before creation and returns attachment JSON), `cleanup-staged-bill-attachments`, `create-bill` (`--payload`; set `isClaimable=false` for a bill, `isClaimable=true` for a claim; supplier bills require positive `extractedAmount` and `amount`, with `amount <= extractedAmount`; an already-paid supplier bill sets `alreadyPaid=true` and requires `paymentReference` plus a staged PDF in `paymentProofAttachments`, and is created pending finance approval; non-legacy supplier bills from 1 Jul 2026 00:00 SGT also require approved quotation coverage), `update-bill` (`--payload`), `update-bill-payment-evidence` (`--payload`; replaces the payment reference and payment-proof PDFs for an already-paid bill), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status` (`PAID` requires `--paymentReference`; pass `--paymentProof <receipt.pdf>` to stage and submit a PDF up to 20MB atomically, or omit it only when BB already has payment proof; moving to `PAID` runs the server's paid-bill notification workflow), `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clearing a paid bill's QuickBooks link is not allowed), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
91
|
+
| **Quotations** | `list-quotations` (supports project, budget, supplier, status, requester, and text-search filters), `get-quotation`, `upload-quotation-attachment` (`<projectId>` + PDF/JPEG/PNG path up to 20MB; returns attachment JSON for payload use), `cleanup-staged-quotation-attachments`, `create-quotation` (`--payload` for `quotation.createDraft`; include `amountWithoutGst` and `gstAmount`), `update-quotation` (`--payload`; updates a `DRAFT` or `REJECTED` quotation), `delete-quotation`, `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
92
|
+
| **Customer invoices** | `check-customer-invoice-readiness`, `list-customer-invoices`, `get-customer-invoice`, `create-customer-invoice`, `discard-customer-invoice`, `delete-customer-invoice`, `void-customer-invoice`, `approve-customer-invoice`, `reject-customer-invoice`, `download-customer-invoice-pdf`, `sync-customer-invoice` |
|
|
93
|
+
| **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|customer_invoice\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation`, `approve-customer-invoice` / `reject-customer-invoice` |
|
|
94
|
+
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project` (its budget overview returns `totalRevenue`, calculated only from Estimate Accepted and Estimate Closed budgets), `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`, `--projectManagerId`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `projectManagerId` and `requestQboAccountantNotification` in JSON), `delete-project`, `check-project-reconciliation <id>` (runs the web app's live checks without changing status), `reconcile-project <id>` (reruns checks transactionally and marks an eligible project `RECONCILED`), `complete-project <id>` (reruns validation, marks a reconciled project `COMPLETED`, and queues QuickBooks placeholder cleanup), `import-qbo-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `RECONCILED` \| `LOST`; close-out follows `WON` → `RECONCILED` → `COMPLETED`, and reconciliation requires every accepted/closed budget line to have an Approved/Paid bill or claim or be explicitly Not Utilized; marking `COMPLETED` queues deletion of every remaining project placeholder bill from QuickBooks; for `PITCH` → `WON` also pass a Budget Builder user `--projectManagerId` or `--projectManagerEmail`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
|
|
92
95
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
93
96
|
| **Suppliers & items** | `list-suppliers` (defaults to active suppliers, `--perPage 10`, sorted by `createdAt` desc; supports `--name`, `--sortBy` for scalar supplier fields, `--sortDir`, `--createdBy`, `--gstRegistered`, `--status`, `--supplierTags`, `--active false` for archived suppliers), `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin; archives/deactivates related items), `reactivate-suppliers` (`--ids` CSV; admin; reactivates related items), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details` (includes `supplierApprovalSummary` for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
|
|
94
97
|
| **Dashboard & users** | `list-users`, `create-user` (admin; provisions an API-only service identity with no Google sign-in; `--email`, optional `--name`, optional `--role` defaulting to `USER`), `create-api-key` (admin; `--userId`, `--name`; raw key shown once), `list-api-keys` (admin; optional `--userId`), `revoke-api-key` (admin; key ID plus `--userId` for another user's key), `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
|
95
98
|
| **Errors** | `get-recent-errors`, `get-error-metrics` |
|
|
99
|
+
| **Automation (admin)** | `list-integration-operations`, `retry-integration-operation` |
|
|
96
100
|
| **Historical / benchmarks** | `get-approved-budgets`, `get-budget-category-benchmarks`, `get-item-pricing-history`, `get-supplier-pricing-history` |
|
|
97
101
|
|
|
102
|
+
The CLI intentionally wraps low-level upload-request/confirm procedures into file-based commands and omits browser-only helpers such as navigation counts, combobox/facet data, recent-page bookkeeping, and live UI subscriptions. Operational workflows for bills, claims, quotations, customer invoices, project reconciliation/completion, and integration recovery are available directly.
|
|
103
|
+
|
|
98
104
|
## Output
|
|
99
105
|
|
|
100
106
|
- **Stdout:** JSON (pretty-printed)
|
package/dist/api-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createTRPCProxyClient, httpBatchLink, loggerLink } from "@trpc/client";
|
|
2
2
|
import SuperJSON from "superjson";
|
|
3
|
-
import { shouldLogCliActions } from "./cli-trace.js";
|
|
3
|
+
import { sanitizeValueForTrace, shouldLogCliActions } from "./cli-trace.js";
|
|
4
4
|
const BASE_URL = "https://budget-builder.getout.events";
|
|
5
5
|
function getAuthHeader() {
|
|
6
6
|
const key = process.env.BB_API_KEY;
|
|
@@ -26,8 +26,8 @@ export const api = createTRPCProxyClient({
|
|
|
26
26
|
withContext: false,
|
|
27
27
|
// tRPC defaults use console.log for requests; stderr keeps stdout JSON-safe for pipes.
|
|
28
28
|
console: {
|
|
29
|
-
log: (...args) => console.error(...args),
|
|
30
|
-
error: (...args) => console.error(...args),
|
|
29
|
+
log: (...args) => console.error(...args.map((arg) => sanitizeValueForTrace(arg))),
|
|
30
|
+
error: (...args) => console.error(...args.map((arg) => sanitizeValueForTrace(arg))),
|
|
31
31
|
},
|
|
32
32
|
}),
|
|
33
33
|
httpBatchLink({
|
package/dist/cli-trace.js
CHANGED
|
@@ -17,9 +17,39 @@ const SENSITIVE_FLAG = new Set([
|
|
|
17
17
|
"bb_api_key",
|
|
18
18
|
"token",
|
|
19
19
|
"password",
|
|
20
|
+
"payment-proof",
|
|
21
|
+
"paymentproof",
|
|
22
|
+
"paymentreference",
|
|
23
|
+
"receipt",
|
|
20
24
|
"secret",
|
|
21
25
|
"authorization",
|
|
22
26
|
]);
|
|
27
|
+
const SENSITIVE_TRACE_FIELD = new Set([
|
|
28
|
+
"paymentproof",
|
|
29
|
+
"paymentproofattachments",
|
|
30
|
+
"paymentproofpath",
|
|
31
|
+
"paymentreference",
|
|
32
|
+
"receipt",
|
|
33
|
+
]);
|
|
34
|
+
export const sanitizeValueForTrace = (value, seen = new WeakSet()) => {
|
|
35
|
+
if (typeof value !== "object" || value === null)
|
|
36
|
+
return value;
|
|
37
|
+
if (seen.has(value))
|
|
38
|
+
return "[circular]";
|
|
39
|
+
seen.add(value);
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((item) => sanitizeValueForTrace(item, seen));
|
|
42
|
+
}
|
|
43
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
45
|
+
return value;
|
|
46
|
+
return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
|
|
47
|
+
key,
|
|
48
|
+
SENSITIVE_TRACE_FIELD.has(key.toLowerCase())
|
|
49
|
+
? "[redacted]"
|
|
50
|
+
: sanitizeValueForTrace(nestedValue, seen),
|
|
51
|
+
]));
|
|
52
|
+
};
|
|
23
53
|
export const sanitizeFlagsForTrace = (flags) => {
|
|
24
54
|
const out = {};
|
|
25
55
|
for (const [k, v] of Object.entries(flags)) {
|
package/dist/commands.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
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, parseSendEstimateToContactPersonPayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBillPaymentEvidencePayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, parseUpdateQuotationPayload, parseValidateBillSelectionPayload, } from "./parse-mutation-payload.js";
|
|
10
|
+
import { BillStatus, 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",
|
|
@@ -261,6 +262,17 @@ const billAttachmentContentTypeForFileName = (fileName) => {
|
|
|
261
262
|
}
|
|
262
263
|
return resolvedContentType;
|
|
263
264
|
};
|
|
265
|
+
const PAYMENT_PROOF_MAX_SIZE = 20 * 1024 * 1024;
|
|
266
|
+
const QUOTATION_ATTACHMENT_MAX_SIZE = 20 * 1024 * 1024;
|
|
267
|
+
const quotationAttachmentContentTypeForFileName = (fileName) => {
|
|
268
|
+
const resolvedContentType = contentTypeHeaderForFileName(fileName);
|
|
269
|
+
if (resolvedContentType === "application/pdf" ||
|
|
270
|
+
resolvedContentType === "image/jpeg" ||
|
|
271
|
+
resolvedContentType === "image/png") {
|
|
272
|
+
return resolvedContentType;
|
|
273
|
+
}
|
|
274
|
+
throw new Error(`Unsupported quotation attachment type: ${fileName}. Use PDF, JPEG, or PNG.`);
|
|
275
|
+
};
|
|
264
276
|
const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
|
|
265
277
|
BudgetStatus.ESTIMATE_CREATED,
|
|
266
278
|
BudgetStatus.ESTIMATE_SENT,
|
|
@@ -533,6 +545,10 @@ export async function listApprovals(opts) {
|
|
|
533
545
|
id = n.supplierQuotation.id;
|
|
534
546
|
name = `Quotation ${n.supplierQuotation.supplier.name}`;
|
|
535
547
|
}
|
|
548
|
+
else if (n.type === CUSTOMER_INVOICE && n.customerInvoiceBatch) {
|
|
549
|
+
id = n.customerInvoiceBatch.id;
|
|
550
|
+
name = `Customer invoice ${n.customerInvoiceBatch.project.name}`;
|
|
551
|
+
}
|
|
536
552
|
else {
|
|
537
553
|
id = n.id;
|
|
538
554
|
name = "Unknown";
|
|
@@ -593,20 +609,112 @@ export async function createBillApproval(billId) {
|
|
|
593
609
|
email,
|
|
594
610
|
});
|
|
595
611
|
}
|
|
612
|
+
export const validateBillStatusUpdateOptions = (opts) => {
|
|
613
|
+
if (opts.status === BillStatus.PAID && !opts.paymentReference?.trim()) {
|
|
614
|
+
throw new Error("update-bill-status requires --paymentReference when status is PAID.");
|
|
615
|
+
}
|
|
616
|
+
if (opts.status !== BillStatus.PAID && opts.paymentProofPath !== undefined) {
|
|
617
|
+
throw new Error("--paymentProof can only be used when status is PAID.");
|
|
618
|
+
}
|
|
619
|
+
if (opts.paymentProofPath !== undefined && !opts.paymentProofPath.trim()) {
|
|
620
|
+
throw new Error("--paymentProof requires a PDF file path.");
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const deleteStagedPaymentProof = async (projectId, key) => {
|
|
624
|
+
const cleanup = await api.attachment.deleteStagedBillAttachments.mutate({
|
|
625
|
+
projectId,
|
|
626
|
+
keys: [key],
|
|
627
|
+
});
|
|
628
|
+
if (cleanup.failedKeys.includes(key)) {
|
|
629
|
+
throw new Error(`Failed to delete staged payment proof ${key}.`);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
export const rethrowAfterStagedPaymentProofCleanup = async (error, cleanup) => {
|
|
633
|
+
if (cleanup) {
|
|
634
|
+
try {
|
|
635
|
+
await cleanup();
|
|
636
|
+
}
|
|
637
|
+
catch (cleanupError) {
|
|
638
|
+
throw new AggregateError([error, cleanupError], "Bill status update failed and its staged payment proof could not be cleaned up.");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
throw error;
|
|
642
|
+
};
|
|
643
|
+
const stageBillPaymentProof = async (projectId, filePath) => {
|
|
644
|
+
const buffer = await readFile(filePath);
|
|
645
|
+
const fileName = basename(filePath);
|
|
646
|
+
const size = buffer.byteLength;
|
|
647
|
+
const contentType = billAttachmentContentTypeForFileName(fileName);
|
|
648
|
+
if (!fileName.toLowerCase().endsWith(".pdf") ||
|
|
649
|
+
contentType !== "application/pdf") {
|
|
650
|
+
throw new Error("Payment proof must be a PDF.");
|
|
651
|
+
}
|
|
652
|
+
if (size === 0) {
|
|
653
|
+
throw new Error("Payment proof must not be empty.");
|
|
654
|
+
}
|
|
655
|
+
if (size > PAYMENT_PROOF_MAX_SIZE) {
|
|
656
|
+
throw new Error("Payment proof must be 20MB or smaller.");
|
|
657
|
+
}
|
|
658
|
+
const { uploadUrl, key } = await api.attachment.requestStagedBillAttachmentUpload.mutate({
|
|
659
|
+
projectId,
|
|
660
|
+
fileName,
|
|
661
|
+
size,
|
|
662
|
+
contentType,
|
|
663
|
+
});
|
|
664
|
+
try {
|
|
665
|
+
const response = await fetch(uploadUrl, {
|
|
666
|
+
method: "PUT",
|
|
667
|
+
body: buffer,
|
|
668
|
+
headers: { "Content-Type": contentType },
|
|
669
|
+
});
|
|
670
|
+
if (!response.ok) {
|
|
671
|
+
throw new Error(`S3 upload failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch (uploadError) {
|
|
675
|
+
try {
|
|
676
|
+
await deleteStagedPaymentProof(projectId, key);
|
|
677
|
+
}
|
|
678
|
+
catch (cleanupError) {
|
|
679
|
+
throw new AggregateError([uploadError, cleanupError], "Payment-proof upload failed and its staged object could not be cleaned up.");
|
|
680
|
+
}
|
|
681
|
+
throw uploadError;
|
|
682
|
+
}
|
|
683
|
+
return { id: randomUUID(), key, name: fileName, size };
|
|
684
|
+
};
|
|
596
685
|
export async function updateBillStatus(opts) {
|
|
686
|
+
validateBillStatusUpdateOptions(opts);
|
|
597
687
|
await assertSensitiveWorkflowConfirmed({
|
|
598
688
|
action: "Update bill status",
|
|
599
689
|
entity: `bill ${opts.id}`,
|
|
600
690
|
details: `to ${opts.status}`,
|
|
601
691
|
});
|
|
602
|
-
const
|
|
603
|
-
id: opts.id
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
692
|
+
const bill = opts.paymentProofPath
|
|
693
|
+
? await api.bill.getById.query({ id: opts.id })
|
|
694
|
+
: null;
|
|
695
|
+
const paymentProof = bill && opts.paymentProofPath
|
|
696
|
+
? await stageBillPaymentProof(bill.projectId, opts.paymentProofPath)
|
|
697
|
+
: undefined;
|
|
698
|
+
try {
|
|
699
|
+
const result = await api.bill.updateStatus.mutate({
|
|
700
|
+
id: opts.id,
|
|
701
|
+
status: opts.status,
|
|
702
|
+
rejectionReason: opts.rejectionReason,
|
|
703
|
+
paymentTrackingUrl: opts.paymentTrackingUrl,
|
|
704
|
+
paymentReference: opts.paymentReference,
|
|
705
|
+
paymentProofAttachments: paymentProof ? [paymentProof] : undefined,
|
|
706
|
+
});
|
|
707
|
+
out(result);
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
await rethrowAfterStagedPaymentProofCleanup(error, bill && paymentProof
|
|
711
|
+
? async () => {
|
|
712
|
+
// The cleanup endpoint checks Attachment rows and refuses to delete a key
|
|
713
|
+
// that an ambiguously successful status update already attached.
|
|
714
|
+
await deleteStagedPaymentProof(bill.projectId, paymentProof.key);
|
|
715
|
+
}
|
|
716
|
+
: undefined);
|
|
717
|
+
}
|
|
610
718
|
}
|
|
611
719
|
export async function patchBillPayment(opts) {
|
|
612
720
|
const input = { id: opts.id };
|
|
@@ -668,6 +776,13 @@ export async function stageBillAttachmentsFromPaths(projectId, filePaths) {
|
|
|
668
776
|
attachments,
|
|
669
777
|
});
|
|
670
778
|
}
|
|
779
|
+
export const cleanupStagedBillAttachments = async (projectId, keys) => {
|
|
780
|
+
const result = await api.attachment.deleteStagedBillAttachments.mutate({
|
|
781
|
+
projectId,
|
|
782
|
+
keys,
|
|
783
|
+
});
|
|
784
|
+
out(result);
|
|
785
|
+
};
|
|
671
786
|
export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
|
|
672
787
|
const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
|
|
673
788
|
out(confirmed);
|
|
@@ -740,6 +855,10 @@ export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [],
|
|
|
740
855
|
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
741
856
|
const buf = await readFile(filePath);
|
|
742
857
|
const fileName = basename(filePath);
|
|
858
|
+
if (buf.byteLength > QUOTATION_ATTACHMENT_MAX_SIZE) {
|
|
859
|
+
throw new Error("Quotation attachment must be 20MB or smaller.");
|
|
860
|
+
}
|
|
861
|
+
const attachmentContentType = quotationAttachmentContentTypeForFileName(fileName);
|
|
743
862
|
const ext = fileName
|
|
744
863
|
.split(".")
|
|
745
864
|
.pop()
|
|
@@ -752,7 +871,7 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
|
752
871
|
const res = await fetch(uploadUrl, {
|
|
753
872
|
method: "PUT",
|
|
754
873
|
body: buf,
|
|
755
|
-
headers: { "Content-Type":
|
|
874
|
+
headers: { "Content-Type": attachmentContentType },
|
|
756
875
|
});
|
|
757
876
|
if (!res.ok) {
|
|
758
877
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
@@ -764,6 +883,13 @@ export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
|
764
883
|
size: buf.byteLength,
|
|
765
884
|
});
|
|
766
885
|
}
|
|
886
|
+
export const cleanupStagedQuotationAttachments = async (projectId, keys) => {
|
|
887
|
+
const result = await api.quotation.deleteStagedAttachments.mutate({
|
|
888
|
+
projectId,
|
|
889
|
+
keys,
|
|
890
|
+
});
|
|
891
|
+
out(result);
|
|
892
|
+
};
|
|
767
893
|
export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
768
894
|
await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
|
|
769
895
|
const buf = await readFile(filePath);
|
|
@@ -928,6 +1054,17 @@ export async function createEstimate(budgetId, preflight) {
|
|
|
928
1054
|
const result = await api.quickbooks.createEstimate.mutate({ budgetId });
|
|
929
1055
|
out(result);
|
|
930
1056
|
}
|
|
1057
|
+
export const describeEstimateEmailConfirmation = (input) => `to ${input.to}; CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; attach the QBO estimate PDF, terms and conditions, and Budget Builder attachments, then mark the estimate sent`;
|
|
1058
|
+
export const sendEstimateToContactPersonFromPayload = async (raw) => {
|
|
1059
|
+
const input = parseSendEstimateToContactPersonPayload(raw);
|
|
1060
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1061
|
+
action: "Send estimate email",
|
|
1062
|
+
entity: `budget ${input.budgetId}`,
|
|
1063
|
+
details: describeEstimateEmailConfirmation(input),
|
|
1064
|
+
});
|
|
1065
|
+
const result = await api.email.sendEstimateToContactPerson.mutate(input);
|
|
1066
|
+
out(result);
|
|
1067
|
+
};
|
|
931
1068
|
export async function getBudgetDetails(budgetId) {
|
|
932
1069
|
const detail = await api.budget.getBudgetDetail.query({ id: budgetId });
|
|
933
1070
|
out(detail);
|
|
@@ -990,6 +1127,68 @@ export async function updateBudgetDiscountFromPayload(raw) {
|
|
|
990
1127
|
const result = await api.budget.updateBudgetDiscount.mutate(input);
|
|
991
1128
|
out(result);
|
|
992
1129
|
}
|
|
1130
|
+
export const deleteBudgetDiscount = async (budgetId) => {
|
|
1131
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1132
|
+
action: "Remove budget discount",
|
|
1133
|
+
entity: `budget ${budgetId}`,
|
|
1134
|
+
details: "and return the budget to DRAFT",
|
|
1135
|
+
});
|
|
1136
|
+
const result = await api.budget.deleteBudgetDiscount.mutate({ budgetId });
|
|
1137
|
+
out(result);
|
|
1138
|
+
};
|
|
1139
|
+
export const deleteBudgetCommission = async (budgetId) => {
|
|
1140
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1141
|
+
action: "Remove budget commission",
|
|
1142
|
+
entity: `budget ${budgetId}`,
|
|
1143
|
+
details: "and return the budget to DRAFT",
|
|
1144
|
+
});
|
|
1145
|
+
const result = await api.budget.deleteBudgetCommission.mutate({ budgetId });
|
|
1146
|
+
out(result);
|
|
1147
|
+
};
|
|
1148
|
+
export const setBudgetItemsNotUtilized = async (budgetItemIds, notUtilized) => {
|
|
1149
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1150
|
+
action: notUtilized
|
|
1151
|
+
? "Mark budget lines Not Utilized"
|
|
1152
|
+
: "Restore budget line",
|
|
1153
|
+
entity: `budget item${budgetItemIds.length === 1 ? "" : "s"} ${budgetItemIds.join(", ")}`,
|
|
1154
|
+
details: notUtilized
|
|
1155
|
+
? "and remove linked QuickBooks placeholder expenses"
|
|
1156
|
+
: "and restore its QuickBooks placeholder expense when required",
|
|
1157
|
+
});
|
|
1158
|
+
const result = await api.budget.setBudgetItemsNotUtilized.mutate({
|
|
1159
|
+
budgetItemIds,
|
|
1160
|
+
notUtilized,
|
|
1161
|
+
});
|
|
1162
|
+
out(result);
|
|
1163
|
+
};
|
|
1164
|
+
export const createPlaceholderBillForBudgetItem = async (budgetItemId) => {
|
|
1165
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1166
|
+
action: "Create QuickBooks placeholder bill",
|
|
1167
|
+
entity: `budget item ${budgetItemId}`,
|
|
1168
|
+
});
|
|
1169
|
+
const result = await api.budget.createPlaceholderBillForBudgetItem.mutate({
|
|
1170
|
+
budgetItemId,
|
|
1171
|
+
});
|
|
1172
|
+
out(result);
|
|
1173
|
+
};
|
|
1174
|
+
export const renameBudgetVersion = async (versionId, newName) => {
|
|
1175
|
+
const result = await api.budgetVersion.renameBudgetVersion.mutate({
|
|
1176
|
+
versionId,
|
|
1177
|
+
newName,
|
|
1178
|
+
});
|
|
1179
|
+
out(result);
|
|
1180
|
+
};
|
|
1181
|
+
export const restoreBudgetVersion = async (versionId) => {
|
|
1182
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1183
|
+
action: "Restore budget version",
|
|
1184
|
+
entity: `budget version ${versionId}`,
|
|
1185
|
+
details: "replace the current budget, project fields, items, and attachments, clear its QuickBooks estimate link, and return it to DRAFT",
|
|
1186
|
+
});
|
|
1187
|
+
const result = await api.budgetVersion.restoreBudgetVersion.mutate({
|
|
1188
|
+
versionId,
|
|
1189
|
+
});
|
|
1190
|
+
out(result);
|
|
1191
|
+
};
|
|
993
1192
|
export async function deleteItemById(itemId) {
|
|
994
1193
|
const result = await api.item.deleteItem.mutate({ id: itemId });
|
|
995
1194
|
out(result);
|
|
@@ -1044,9 +1243,19 @@ export async function getQuotationDetails(id) {
|
|
|
1044
1243
|
out(result);
|
|
1045
1244
|
}
|
|
1046
1245
|
export async function createQuotationFromPayload(raw) {
|
|
1047
|
-
const
|
|
1246
|
+
const input = parseCreateQuotationPayload(raw);
|
|
1247
|
+
const result = await api.quotation.createDraft.mutate(input);
|
|
1048
1248
|
out(result);
|
|
1049
1249
|
}
|
|
1250
|
+
export const updateQuotationFromPayload = async (raw) => {
|
|
1251
|
+
const input = parseUpdateQuotationPayload(raw);
|
|
1252
|
+
const result = await api.quotation.updateDraftOrRejected.mutate(input);
|
|
1253
|
+
out(result);
|
|
1254
|
+
};
|
|
1255
|
+
export const deleteQuotationById = async (id) => {
|
|
1256
|
+
const result = await api.quotation.delete.mutate({ id });
|
|
1257
|
+
out(result);
|
|
1258
|
+
};
|
|
1050
1259
|
export async function submitQuotation(id) {
|
|
1051
1260
|
await assertSensitiveWorkflowConfirmed({
|
|
1052
1261
|
action: "Submit quotation for approval",
|
|
@@ -1062,11 +1271,121 @@ export async function downloadQuotationPdf(id, fileRole) {
|
|
|
1062
1271
|
});
|
|
1063
1272
|
out(result);
|
|
1064
1273
|
}
|
|
1274
|
+
export const checkCustomerInvoiceReadiness = async (budgetId) => {
|
|
1275
|
+
const result = await api.customerInvoice.getInvoiceReadiness.query({
|
|
1276
|
+
budgetId,
|
|
1277
|
+
});
|
|
1278
|
+
out(result);
|
|
1279
|
+
};
|
|
1280
|
+
export const listCustomerInvoices = async (input) => {
|
|
1281
|
+
const result = await api.customerInvoice.listByBudget.query(input);
|
|
1282
|
+
out(result);
|
|
1283
|
+
};
|
|
1284
|
+
export const getCustomerInvoice = async (batchId) => {
|
|
1285
|
+
const result = await api.customerInvoice.getInvoiceDetail.query({ batchId });
|
|
1286
|
+
out(result);
|
|
1287
|
+
};
|
|
1288
|
+
export const createCustomerInvoice = async (raw) => {
|
|
1289
|
+
const input = parseCreateCustomerInvoicePayload(raw);
|
|
1290
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1291
|
+
action: "Create customer invoice",
|
|
1292
|
+
entity: `budget ${input.budgetId}`,
|
|
1293
|
+
details: "create the invoice in QuickBooks and either approve it as admin or send approval-request emails",
|
|
1294
|
+
});
|
|
1295
|
+
const result = await api.customerInvoice.createInvoiceBatch.mutate(input);
|
|
1296
|
+
out(result);
|
|
1297
|
+
};
|
|
1298
|
+
export const discardCreatingCustomerInvoice = async (batchId) => {
|
|
1299
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1300
|
+
action: "Discard unfinished customer invoice",
|
|
1301
|
+
entity: `invoice batch ${batchId}`,
|
|
1302
|
+
});
|
|
1303
|
+
const result = await api.customerInvoice.discardCreatingInvoiceBatch.mutate({
|
|
1304
|
+
batchId,
|
|
1305
|
+
});
|
|
1306
|
+
out(result);
|
|
1307
|
+
};
|
|
1308
|
+
export const deleteCustomerInvoice = async (batchId) => {
|
|
1309
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1310
|
+
action: "Delete customer invoice",
|
|
1311
|
+
entity: `invoice batch ${batchId}`,
|
|
1312
|
+
details: "delete its invoices from QuickBooks and remove the local batch",
|
|
1313
|
+
});
|
|
1314
|
+
const result = await api.customerInvoice.deleteInvoiceBatch.mutate({
|
|
1315
|
+
batchId,
|
|
1316
|
+
});
|
|
1317
|
+
out(result);
|
|
1318
|
+
};
|
|
1319
|
+
export const voidCustomerInvoice = async (batchId) => {
|
|
1320
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1321
|
+
action: "Void customer invoice",
|
|
1322
|
+
entity: `invoice batch ${batchId}`,
|
|
1323
|
+
details: "void its invoices in QuickBooks",
|
|
1324
|
+
});
|
|
1325
|
+
const result = await api.customerInvoice.voidInvoiceBatch.mutate({ batchId });
|
|
1326
|
+
out(result);
|
|
1327
|
+
};
|
|
1328
|
+
export const approveCustomerInvoice = async (batchId) => {
|
|
1329
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1330
|
+
action: "Approve customer invoice",
|
|
1331
|
+
entity: `invoice batch ${batchId}`,
|
|
1332
|
+
details: "and notify its creator",
|
|
1333
|
+
});
|
|
1334
|
+
const result = await api.customerInvoice.approveInvoiceBatch.mutate({
|
|
1335
|
+
batchId,
|
|
1336
|
+
});
|
|
1337
|
+
out(result);
|
|
1338
|
+
};
|
|
1339
|
+
export const rejectCustomerInvoice = async (batchId, rejectionReason) => {
|
|
1340
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1341
|
+
action: "Reject customer invoice",
|
|
1342
|
+
entity: `invoice batch ${batchId}`,
|
|
1343
|
+
details: `with reason "${rejectionReason}", void it in QuickBooks, and notify its creator`,
|
|
1344
|
+
});
|
|
1345
|
+
const result = await api.customerInvoice.rejectInvoiceBatch.mutate({
|
|
1346
|
+
batchId,
|
|
1347
|
+
rejectionReason,
|
|
1348
|
+
});
|
|
1349
|
+
out(result);
|
|
1350
|
+
};
|
|
1351
|
+
export const downloadCustomerInvoicePdf = async (invoiceId, outputPath) => {
|
|
1352
|
+
const result = await api.customerInvoice.downloadPdf.mutate({ invoiceId });
|
|
1353
|
+
const resolvedOutputPath = outputPath?.trim() || result.fileName;
|
|
1354
|
+
const contents = Buffer.from(result.base64, "base64");
|
|
1355
|
+
await writeFile(resolvedOutputPath, contents);
|
|
1356
|
+
out({
|
|
1357
|
+
contentType: result.contentType,
|
|
1358
|
+
fileName: result.fileName,
|
|
1359
|
+
outputPath: resolvedOutputPath,
|
|
1360
|
+
size: contents.byteLength,
|
|
1361
|
+
});
|
|
1362
|
+
};
|
|
1363
|
+
export const syncCustomerInvoice = async (invoiceId) => {
|
|
1364
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1365
|
+
action: "Sync customer invoice from QuickBooks",
|
|
1366
|
+
entity: `invoice ${invoiceId}`,
|
|
1367
|
+
details: "and update its local status, QuickBooks metadata, balance, and history",
|
|
1368
|
+
});
|
|
1369
|
+
const result = await api.customerInvoice.syncInvoiceStatus.mutate({
|
|
1370
|
+
invoiceId,
|
|
1371
|
+
});
|
|
1372
|
+
out(result);
|
|
1373
|
+
};
|
|
1065
1374
|
export async function updateBillFromPayload(raw) {
|
|
1066
1375
|
const input = parseUpdateBillPayload(raw);
|
|
1067
1376
|
const result = await api.bill.update.mutate(input);
|
|
1068
1377
|
out(result);
|
|
1069
1378
|
}
|
|
1379
|
+
export const validateBillSelectionFromPayload = async (raw) => {
|
|
1380
|
+
const input = parseValidateBillSelectionPayload(raw);
|
|
1381
|
+
const result = await api.bill.validateSelection.mutate(input);
|
|
1382
|
+
out(result);
|
|
1383
|
+
};
|
|
1384
|
+
export const updateBillPaymentEvidenceFromPayload = async (raw) => {
|
|
1385
|
+
const input = parseUpdateBillPaymentEvidencePayload(raw);
|
|
1386
|
+
const result = await api.bill.updatePaymentEvidence.mutate(input);
|
|
1387
|
+
out(result);
|
|
1388
|
+
};
|
|
1070
1389
|
export async function deleteBillById(id) {
|
|
1071
1390
|
const result = await api.bill.delete.mutate({ id });
|
|
1072
1391
|
out(result);
|
|
@@ -1576,6 +1895,47 @@ export async function updateProjectStatus(id, status, opts) {
|
|
|
1576
1895
|
});
|
|
1577
1896
|
out(result);
|
|
1578
1897
|
}
|
|
1898
|
+
export const checkProjectReconciliation = async (projectId) => {
|
|
1899
|
+
const result = await api.project.getReconciliationReview.query({ projectId });
|
|
1900
|
+
out(result);
|
|
1901
|
+
};
|
|
1902
|
+
export const reconcileProject = async (projectId) => {
|
|
1903
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1904
|
+
action: "Reconcile project",
|
|
1905
|
+
entity: `project ${projectId}`,
|
|
1906
|
+
details: "rerun all reconciliation checks and mark it RECONCILED",
|
|
1907
|
+
});
|
|
1908
|
+
const result = await api.project.reconcile.mutate({ projectId });
|
|
1909
|
+
out(result);
|
|
1910
|
+
};
|
|
1911
|
+
export const completeProject = async (projectId) => {
|
|
1912
|
+
await updateProjectStatus(projectId, ProjectStatus.COMPLETED);
|
|
1913
|
+
};
|
|
1914
|
+
export const importQuickBooksProjectId = async (projectId, projectUrl) => {
|
|
1915
|
+
const result = await api.project.importQboProjectId.mutate({
|
|
1916
|
+
projectId,
|
|
1917
|
+
projectUrl,
|
|
1918
|
+
});
|
|
1919
|
+
out(result);
|
|
1920
|
+
};
|
|
1921
|
+
export const listIntegrationOperations = async (input) => {
|
|
1922
|
+
const result = await api.integration.listOperations.query(input);
|
|
1923
|
+
out(result);
|
|
1924
|
+
};
|
|
1925
|
+
export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled) => {
|
|
1926
|
+
await assertSensitiveWorkflowConfirmed({
|
|
1927
|
+
action: "Retry integration operation",
|
|
1928
|
+
entity: `operation ${operationId}`,
|
|
1929
|
+
details: confirmExternalStateReconciled
|
|
1930
|
+
? "after confirming the external state has been reconciled"
|
|
1931
|
+
: undefined,
|
|
1932
|
+
});
|
|
1933
|
+
const result = await api.integration.retryOperation.mutate({
|
|
1934
|
+
operationId,
|
|
1935
|
+
confirmExternalStateReconciled,
|
|
1936
|
+
});
|
|
1937
|
+
out(result);
|
|
1938
|
+
};
|
|
1579
1939
|
// --- Contacts ---
|
|
1580
1940
|
export async function listContacts(companyId) {
|
|
1581
1941
|
const contacts = await api.contactPerson.getContactPersonByCompanyId.query({
|