@go-labs-sg/bb 2.20.0 → 2.24.1
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 +58 -27
- package/command-manifest.json +771 -0
- package/command-reference.md +25 -0
- package/dist/index.js +21162 -1995
- package/package.json +2 -2
- package/role-aware-agent-guide.md +2 -2
- package/dist/api-client.js +0 -93
- package/dist/cli-trace.js +0 -86
- package/dist/commands.js +0 -2299
- package/dist/filter-enums.js +0 -58
- package/dist/load-env.js +0 -6
- package/dist/parse-args.js +0 -55
- package/dist/parse-cli-enums.js +0 -336
- package/dist/parse-json-flag.js +0 -20
- package/dist/parse-mutation-payload.js +0 -363
- package/dist/prisma-enums.js +0 -81
- package/dist/registry/generate-command-artifacts.js +0 -36
- package/dist/registry/index.js +0 -645
- package/dist/rich-text.js +0 -34
- package/dist/runtime/confirmation.js +0 -76
- package/dist/runtime/error.js +0 -143
- package/dist/runtime/index.js +0 -6
- package/dist/runtime/output.js +0 -36
- package/dist/runtime/process-runtime.js +0 -28
- package/dist/runtime/sanitize.js +0 -50
- package/dist/runtime/session.js +0 -50
- package/dist/runtime/types.js +0 -1
package/dist/commands.js
DELETED
|
@@ -1,2299 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename } from "node:path";
|
|
4
|
-
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { contentType } from "mime-types";
|
|
6
|
-
import { api } from "./api-client.js";
|
|
7
|
-
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
|
|
8
|
-
import { billStatusesForApi, } from "./parse-cli-enums.js";
|
|
9
|
-
import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseCreateCustomerInvoicePayload, parseCreateQuotationPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSendCustomerInvoiceToContactPersonPayload, 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
|
-
import { createRichTextFromPlainText } from "./rich-text.js";
|
|
12
|
-
import { confirmCurrentCommand, emitCommandResult, getActiveCliSession, } from "./runtime/index.js";
|
|
13
|
-
const BUDGET = "BUDGET";
|
|
14
|
-
const BILL = "BILL";
|
|
15
|
-
const SUPPLIER = "SUPPLIER";
|
|
16
|
-
const QUOTATION = "QUOTATION";
|
|
17
|
-
const CUSTOMER_INVOICE = "CUSTOMER_INVOICE";
|
|
18
|
-
const ASANA_WON_LOST_SECTION_GIDS = new Set([
|
|
19
|
-
"1211678338364908",
|
|
20
|
-
"1211678338364907",
|
|
21
|
-
]);
|
|
22
|
-
const MAX_ASANA_LEAD_CANDIDATES = 5;
|
|
23
|
-
const ASANA_SEARCH_DETAIL_LIMIT = 15;
|
|
24
|
-
const NOTIFICATION_PAGE_LIMIT = 50;
|
|
25
|
-
const getAllNotifications = async () => {
|
|
26
|
-
const notifications = [];
|
|
27
|
-
let cursor;
|
|
28
|
-
do {
|
|
29
|
-
const page = await api.budget.getNotifications.query({
|
|
30
|
-
cursor,
|
|
31
|
-
limit: NOTIFICATION_PAGE_LIMIT,
|
|
32
|
-
});
|
|
33
|
-
notifications.push(...page.notifications);
|
|
34
|
-
cursor = page.nextCursor;
|
|
35
|
-
} while (cursor);
|
|
36
|
-
return notifications;
|
|
37
|
-
};
|
|
38
|
-
const createApprovalEmailDeliveryReport = (delivery) => {
|
|
39
|
-
if (!delivery) {
|
|
40
|
-
return {
|
|
41
|
-
queued: false,
|
|
42
|
-
sent: false,
|
|
43
|
-
count: 0,
|
|
44
|
-
queuedCount: 0,
|
|
45
|
-
sentCount: 0,
|
|
46
|
-
skippedCount: 0,
|
|
47
|
-
error: "No approval email delivery was queued",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
const { queuedCount, sentCount, skippedCount, message, operationIds } = delivery;
|
|
51
|
-
return {
|
|
52
|
-
queued: queuedCount > 0,
|
|
53
|
-
sent: false,
|
|
54
|
-
count: queuedCount,
|
|
55
|
-
queuedCount,
|
|
56
|
-
sentCount,
|
|
57
|
-
skippedCount,
|
|
58
|
-
operationIds,
|
|
59
|
-
message,
|
|
60
|
-
};
|
|
61
|
-
};
|
|
62
|
-
const createApprovalEmailDeliveryFailure = (error) => ({
|
|
63
|
-
queued: false,
|
|
64
|
-
sent: false,
|
|
65
|
-
count: 0,
|
|
66
|
-
queuedCount: 0,
|
|
67
|
-
sentCount: 0,
|
|
68
|
-
skippedCount: 0,
|
|
69
|
-
error,
|
|
70
|
-
});
|
|
71
|
-
function isPendingRequest(n) {
|
|
72
|
-
return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
|
|
73
|
-
}
|
|
74
|
-
const out = (data) => {
|
|
75
|
-
emitCommandResult(data);
|
|
76
|
-
};
|
|
77
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
-
const stringField = (record, key) => {
|
|
79
|
-
const value = record[key];
|
|
80
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
81
|
-
};
|
|
82
|
-
const asanaSectionsForTask = (task) => {
|
|
83
|
-
if (!isRecord(task) || !Array.isArray(task.memberships))
|
|
84
|
-
return [];
|
|
85
|
-
return task.memberships.flatMap((membership) => {
|
|
86
|
-
if (!isRecord(membership) || !isRecord(membership.section))
|
|
87
|
-
return [];
|
|
88
|
-
return [
|
|
89
|
-
{
|
|
90
|
-
gid: stringField(membership.section, "gid"),
|
|
91
|
-
name: stringField(membership.section, "name"),
|
|
92
|
-
},
|
|
93
|
-
];
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
|
-
const isWonOrLostAsanaTask = (task) => asanaSectionsForTask(task).some((section) => {
|
|
97
|
-
if (section.gid && ASANA_WON_LOST_SECTION_GIDS.has(section.gid)) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
const name = section.name?.trim().toLowerCase();
|
|
101
|
-
return Boolean(name && /\b(won|lost)\b/.test(name));
|
|
102
|
-
});
|
|
103
|
-
const asanaCandidateFromTask = (task, details) => {
|
|
104
|
-
const source = details ?? task;
|
|
105
|
-
const sourceRecord = isRecord(source) ? source : undefined;
|
|
106
|
-
const gid = (sourceRecord ? stringField(sourceRecord, "gid") : undefined) ?? task.gid;
|
|
107
|
-
const name = (sourceRecord ? stringField(sourceRecord, "name") : undefined) ??
|
|
108
|
-
task.name ??
|
|
109
|
-
gid;
|
|
110
|
-
if (!gid || isWonOrLostAsanaTask(source))
|
|
111
|
-
return null;
|
|
112
|
-
return {
|
|
113
|
-
gid,
|
|
114
|
-
name,
|
|
115
|
-
modifiedAt: (sourceRecord ? stringField(sourceRecord, "modified_at") : undefined) ??
|
|
116
|
-
task.modified_at,
|
|
117
|
-
createdAt: (sourceRecord ? stringField(sourceRecord, "created_at") : undefined) ??
|
|
118
|
-
task.created_at,
|
|
119
|
-
sections: asanaSectionsForTask(source),
|
|
120
|
-
};
|
|
121
|
-
};
|
|
122
|
-
const uniqueSearchQueries = (...queries) => {
|
|
123
|
-
const seen = new Set();
|
|
124
|
-
const unique = [];
|
|
125
|
-
for (const query of queries) {
|
|
126
|
-
const normalized = query?.trim() ?? "";
|
|
127
|
-
if (!normalized)
|
|
128
|
-
continue;
|
|
129
|
-
const key = normalized.toLowerCase();
|
|
130
|
-
if (seen.has(key))
|
|
131
|
-
continue;
|
|
132
|
-
seen.add(key);
|
|
133
|
-
unique.push(normalized);
|
|
134
|
-
}
|
|
135
|
-
return unique;
|
|
136
|
-
};
|
|
137
|
-
const searchOpenAsanaLeadCandidates = async ({ projectName, searchQuery, }) => {
|
|
138
|
-
const queries = uniqueSearchQueries(projectName, searchQuery);
|
|
139
|
-
const candidates = [];
|
|
140
|
-
const seenTaskIds = new Set();
|
|
141
|
-
for (const query of queries) {
|
|
142
|
-
const results = await api.asana.searchTasks.query({ query });
|
|
143
|
-
const tasks = results.data.slice(0, ASANA_SEARCH_DETAIL_LIMIT);
|
|
144
|
-
const detailsByTaskId = new Map();
|
|
145
|
-
await Promise.all(tasks.map(async (task) => {
|
|
146
|
-
if (!task.gid)
|
|
147
|
-
return;
|
|
148
|
-
try {
|
|
149
|
-
const details = await api.asana.getTaskById.query({ id: task.gid });
|
|
150
|
-
detailsByTaskId.set(task.gid, details.data);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// Keep the compact search result if detail lookup fails for one task.
|
|
154
|
-
}
|
|
155
|
-
}));
|
|
156
|
-
for (const task of tasks) {
|
|
157
|
-
if (!task.gid || seenTaskIds.has(task.gid))
|
|
158
|
-
continue;
|
|
159
|
-
const candidate = asanaCandidateFromTask(task, detailsByTaskId.get(task.gid));
|
|
160
|
-
if (!candidate)
|
|
161
|
-
continue;
|
|
162
|
-
seenTaskIds.add(candidate.gid);
|
|
163
|
-
candidates.push(candidate);
|
|
164
|
-
if (candidates.length >= MAX_ASANA_LEAD_CANDIDATES)
|
|
165
|
-
return candidates;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return candidates;
|
|
169
|
-
};
|
|
170
|
-
const formatAsanaCandidateMetadata = (candidate) => {
|
|
171
|
-
const sectionText = candidate.sections
|
|
172
|
-
.map((section) => section.name ?? section.gid)
|
|
173
|
-
.filter(Boolean)
|
|
174
|
-
.join(", ") || "stage unavailable";
|
|
175
|
-
const dateText = candidate.modifiedAt
|
|
176
|
-
? `modified ${new Date(candidate.modifiedAt).toISOString()}`
|
|
177
|
-
: candidate.createdAt
|
|
178
|
-
? `created ${new Date(candidate.createdAt).toISOString()}`
|
|
179
|
-
: "date unavailable";
|
|
180
|
-
return `${sectionText}; ${dateText}; ${candidate.gid}`;
|
|
181
|
-
};
|
|
182
|
-
const promptForAsanaLeadCandidate = async (candidates) => {
|
|
183
|
-
if (candidates.length === 0) {
|
|
184
|
-
throw new Error("No open Asana lead tasks were found in the Deals project. Pass --asanaTaskId explicitly or update the Deals project first.");
|
|
185
|
-
}
|
|
186
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
187
|
-
throw new Error("create-project requires --asanaTaskId in non-interactive runs. Interactive runs can omit it to choose from the top Asana lead task matches.");
|
|
188
|
-
}
|
|
189
|
-
console.error("Select the Asana lead task to link to this project:");
|
|
190
|
-
candidates.forEach((candidate, index) => {
|
|
191
|
-
console.error(`${index + 1}. ${candidate.name} (${formatAsanaCandidateMetadata(candidate)})`);
|
|
192
|
-
});
|
|
193
|
-
const rl = createInterface({
|
|
194
|
-
input: process.stdin,
|
|
195
|
-
output: process.stderr,
|
|
196
|
-
});
|
|
197
|
-
try {
|
|
198
|
-
while (true) {
|
|
199
|
-
const answer = await rl.question(`Choose 1-${candidates.length}, or q to abort: `);
|
|
200
|
-
const trimmed = answer.trim().toLowerCase();
|
|
201
|
-
if (trimmed === "q" || trimmed === "quit") {
|
|
202
|
-
throw new Error("Aborted.");
|
|
203
|
-
}
|
|
204
|
-
const selected = Number.parseInt(trimmed, 10);
|
|
205
|
-
if (Number.isInteger(selected) &&
|
|
206
|
-
selected >= 1 &&
|
|
207
|
-
selected <= candidates.length) {
|
|
208
|
-
const candidate = candidates[selected - 1];
|
|
209
|
-
if (candidate)
|
|
210
|
-
return candidate;
|
|
211
|
-
}
|
|
212
|
-
console.error(`Enter a number from 1 to ${candidates.length}.`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
finally {
|
|
216
|
-
rl.close();
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
const resolveAsanaDealSlackChannel = async (asanaTaskId) => {
|
|
220
|
-
const result = await api.asana.getDealSlackChannel.query({ id: asanaTaskId });
|
|
221
|
-
if (result.status !== "resolved") {
|
|
222
|
-
throw new Error(result.message);
|
|
223
|
-
}
|
|
224
|
-
console.error(`Resolved Slack channel from Asana deal: #${result.channel.slackChannelName} (${result.channel.slackChannelId})`);
|
|
225
|
-
return result.channel;
|
|
226
|
-
};
|
|
227
|
-
const resolveCreateProjectLinks = async (opts) => {
|
|
228
|
-
const asanaTaskId = opts.asanaTaskId?.trim() ||
|
|
229
|
-
(await promptForAsanaLeadCandidate(await searchOpenAsanaLeadCandidates({
|
|
230
|
-
projectName: opts.name,
|
|
231
|
-
searchQuery: opts.asanaSearch,
|
|
232
|
-
}))).gid;
|
|
233
|
-
const hasCompleteManualSlackChannel = Boolean(opts.slackChannelId && opts.slackChannelUrl && opts.slackChannelName);
|
|
234
|
-
if (hasCompleteManualSlackChannel) {
|
|
235
|
-
return {
|
|
236
|
-
asanaTaskId,
|
|
237
|
-
slackChannelId: opts.slackChannelId,
|
|
238
|
-
slackChannelUrl: opts.slackChannelUrl,
|
|
239
|
-
slackChannelName: opts.slackChannelName,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
const slackChannel = await resolveAsanaDealSlackChannel(asanaTaskId);
|
|
243
|
-
return {
|
|
244
|
-
asanaTaskId,
|
|
245
|
-
...slackChannel,
|
|
246
|
-
};
|
|
247
|
-
};
|
|
248
|
-
const combineDateAndTime = (day, time) => {
|
|
249
|
-
const d = new Date(day);
|
|
250
|
-
d.setHours(time.getHours(), time.getMinutes(), time.getSeconds(), 0);
|
|
251
|
-
return d;
|
|
252
|
-
};
|
|
253
|
-
const formatOptionalProjectPaxInput = (pax) => {
|
|
254
|
-
if (typeof pax !== "number" || !Number.isFinite(pax) || pax <= 0) {
|
|
255
|
-
return "";
|
|
256
|
-
}
|
|
257
|
-
return String(pax);
|
|
258
|
-
};
|
|
259
|
-
/** Extension-based MIME, like browser `File.type` when the OS provides it */
|
|
260
|
-
const contentTypeHeaderForFileName = (fileName) => {
|
|
261
|
-
const ct = contentType(fileName);
|
|
262
|
-
return typeof ct === "string" ? ct : "application/octet-stream";
|
|
263
|
-
};
|
|
264
|
-
const supportedAttachmentContentTypes = [
|
|
265
|
-
"application/pdf",
|
|
266
|
-
"image/gif",
|
|
267
|
-
"image/jpeg",
|
|
268
|
-
"image/png",
|
|
269
|
-
"image/webp",
|
|
270
|
-
];
|
|
271
|
-
const attachmentContentTypeForFileName = (fileName) => {
|
|
272
|
-
const resolvedContentType = contentTypeHeaderForFileName(fileName);
|
|
273
|
-
const supportedContentType = supportedAttachmentContentTypes.find((contentTypeValue) => contentTypeValue === resolvedContentType);
|
|
274
|
-
if (supportedContentType)
|
|
275
|
-
return supportedContentType;
|
|
276
|
-
throw new Error(`Unsupported attachment type: ${fileName}`);
|
|
277
|
-
};
|
|
278
|
-
const billAttachmentContentTypeForFileName = (fileName) => {
|
|
279
|
-
const resolvedContentType = attachmentContentTypeForFileName(fileName);
|
|
280
|
-
if (resolvedContentType === "image/webp") {
|
|
281
|
-
throw new Error(`Unsupported QuickBooks bill attachment type: ${fileName}. Use PDF, GIF, JPEG, or PNG.`);
|
|
282
|
-
}
|
|
283
|
-
return resolvedContentType;
|
|
284
|
-
};
|
|
285
|
-
const PAYMENT_PROOF_MAX_SIZE = 20 * 1024 * 1024;
|
|
286
|
-
const QUOTATION_ATTACHMENT_MAX_SIZE = 20 * 1024 * 1024;
|
|
287
|
-
const quotationAttachmentContentTypeForFileName = (fileName) => {
|
|
288
|
-
const resolvedContentType = contentTypeHeaderForFileName(fileName);
|
|
289
|
-
if (resolvedContentType === "application/pdf" ||
|
|
290
|
-
resolvedContentType === "image/jpeg" ||
|
|
291
|
-
resolvedContentType === "image/png") {
|
|
292
|
-
return resolvedContentType;
|
|
293
|
-
}
|
|
294
|
-
throw new Error(`Unsupported quotation attachment type: ${fileName}. Use PDF, JPEG, or PNG.`);
|
|
295
|
-
};
|
|
296
|
-
const LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION = new Set([
|
|
297
|
-
BudgetStatus.ESTIMATE_CREATED,
|
|
298
|
-
BudgetStatus.ESTIMATE_SENT,
|
|
299
|
-
BudgetStatus.ESTIMATE_ACCEPTED,
|
|
300
|
-
BudgetStatus.ESTIMATE_REJECTED,
|
|
301
|
-
BudgetStatus.ESTIMATE_CLOSED,
|
|
302
|
-
]);
|
|
303
|
-
const lockedBudgetStatusLabels = {
|
|
304
|
-
ESTIMATE_CREATED: "Estimate Created",
|
|
305
|
-
ESTIMATE_SENT: "Estimate Sent",
|
|
306
|
-
ESTIMATE_ACCEPTED: "Estimate Accepted",
|
|
307
|
-
ESTIMATE_REJECTED: "Estimate Rejected",
|
|
308
|
-
ESTIMATE_CLOSED: "Estimate Closed",
|
|
309
|
-
};
|
|
310
|
-
const lockedBudgetStatusLabel = (status) => status in lockedBudgetStatusLabels
|
|
311
|
-
? lockedBudgetStatusLabels[status]
|
|
312
|
-
: status;
|
|
313
|
-
export const buildSensitiveWorkflowPreview = ({ action, entity, details, }) => {
|
|
314
|
-
const effects = details ?? "may change workflow state or send notifications";
|
|
315
|
-
return [
|
|
316
|
-
"Budget Builder workflow preview",
|
|
317
|
-
`Action: ${action}`,
|
|
318
|
-
`Target: ${entity}`,
|
|
319
|
-
`Effects: ${effects}`,
|
|
320
|
-
].join("\n");
|
|
321
|
-
};
|
|
322
|
-
const assertSensitiveWorkflowConfirmed = async (input) => {
|
|
323
|
-
if (getActiveCliSession()?.invocation.legacy !== true) {
|
|
324
|
-
await confirmCurrentCommand({
|
|
325
|
-
action: input.action,
|
|
326
|
-
target: input.entity,
|
|
327
|
-
details: input.details,
|
|
328
|
-
});
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
const expected = "CONFIRM";
|
|
332
|
-
const preview = buildSensitiveWorkflowPreview(input);
|
|
333
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
334
|
-
throw new Error(`${preview}\nThis is a sensitive workflow change. Run it in an interactive terminal only after the user explicitly confirms this exact action, then type "${expected}" to continue.`);
|
|
335
|
-
}
|
|
336
|
-
const rl = createInterface({
|
|
337
|
-
input: process.stdin,
|
|
338
|
-
output: process.stderr,
|
|
339
|
-
});
|
|
340
|
-
try {
|
|
341
|
-
const answer = await rl.question(`${preview}\nConfirm the user explicitly approved this exact action, then type "${expected}" to continue: `);
|
|
342
|
-
if (answer.trim() !== expected) {
|
|
343
|
-
throw new Error("Aborted.");
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
finally {
|
|
347
|
-
rl.close();
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
const assertLockedBudgetChangeConfirmed = async ({ budget, action, }) => {
|
|
351
|
-
if (!LOCKED_BUDGET_STATUSES_REQUIRING_CONFIRMATION.has(budget.status)) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const statusLabel = lockedBudgetStatusLabel(budget.status);
|
|
355
|
-
if (getActiveCliSession()?.invocation.legacy !== true) {
|
|
356
|
-
await confirmCurrentCommand({
|
|
357
|
-
action,
|
|
358
|
-
target: `budget ${budget.id}`,
|
|
359
|
-
details: `Changes locked budget "${budget.name}" in status ${statusLabel}.`,
|
|
360
|
-
effects: ["state-change"],
|
|
361
|
-
});
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
365
|
-
throw new Error(`${action} would change locked budget "${budget.name}" (${budget.id}) with status ${statusLabel}. Run this command in an interactive terminal and type "yes" to confirm.`);
|
|
366
|
-
}
|
|
367
|
-
const rl = createInterface({
|
|
368
|
-
input: process.stdin,
|
|
369
|
-
output: process.stderr,
|
|
370
|
-
});
|
|
371
|
-
try {
|
|
372
|
-
const answer = await rl.question(`${action} will change locked budget "${budget.name}" (${budget.id}) with status ${statusLabel}. Type "yes" to continue: `);
|
|
373
|
-
if (answer.trim().toLowerCase() !== "yes") {
|
|
374
|
-
throw new Error("Aborted.");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
finally {
|
|
378
|
-
rl.close();
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
const confirmLockedBudgetChangeByBudgetId = async (budgetId, action) => {
|
|
382
|
-
const budget = await api.budget.getBudget.query({ id: budgetId });
|
|
383
|
-
await assertLockedBudgetChangeConfirmed({
|
|
384
|
-
budget: {
|
|
385
|
-
id: budget.id,
|
|
386
|
-
name: budget.name,
|
|
387
|
-
status: budget.status,
|
|
388
|
-
},
|
|
389
|
-
action,
|
|
390
|
-
});
|
|
391
|
-
};
|
|
392
|
-
const confirmLockedBudgetChangeByBudgetItemId = async (budgetItemId, action) => {
|
|
393
|
-
const { budget } = await api.budgetItem.getBudgetItemContext.query({
|
|
394
|
-
budgetItemId,
|
|
395
|
-
});
|
|
396
|
-
await assertLockedBudgetChangeConfirmed({ budget, action });
|
|
397
|
-
};
|
|
398
|
-
export async function listBudgets(opts) {
|
|
399
|
-
const input = {
|
|
400
|
-
projectId: opts.projectId,
|
|
401
|
-
name: opts.name,
|
|
402
|
-
statuses: opts.statuses,
|
|
403
|
-
createdBy: opts.createdBy,
|
|
404
|
-
dateFrom: opts.dateFrom,
|
|
405
|
-
dateTo: opts.dateTo,
|
|
406
|
-
sortBy: opts.sortBy,
|
|
407
|
-
sortDir: opts.sortDir,
|
|
408
|
-
page: opts.page ?? 1,
|
|
409
|
-
perPage: opts.perPage ?? 20,
|
|
410
|
-
includeDetails: opts.includeDetails ?? true,
|
|
411
|
-
};
|
|
412
|
-
const result = await api.budget.getAllBudgets.query(input);
|
|
413
|
-
out(result);
|
|
414
|
-
}
|
|
415
|
-
export async function getBudget(id) {
|
|
416
|
-
const [budget, items] = await Promise.all([
|
|
417
|
-
api.budget.getBudget.query({ id }),
|
|
418
|
-
api.budget.getBudgetItems.query({ budgetId: id }),
|
|
419
|
-
]);
|
|
420
|
-
out({ budget, items });
|
|
421
|
-
}
|
|
422
|
-
export async function updateBudgetStatus(budgetId, status, opts) {
|
|
423
|
-
await assertSensitiveWorkflowConfirmed({
|
|
424
|
-
action: "Update budget status",
|
|
425
|
-
entity: `budget ${budgetId}`,
|
|
426
|
-
details: `to ${status}`,
|
|
427
|
-
});
|
|
428
|
-
await confirmLockedBudgetChangeByBudgetId(budgetId, "Update budget status");
|
|
429
|
-
const result = await api.budget.updateBudgetStatus.mutate({
|
|
430
|
-
id: budgetId,
|
|
431
|
-
status,
|
|
432
|
-
...(opts?.projectManagerId !== undefined && {
|
|
433
|
-
projectManagerId: opts.projectManagerId,
|
|
434
|
-
}),
|
|
435
|
-
...(opts?.markProjectWon !== undefined && {
|
|
436
|
-
markProjectWon: opts.markProjectWon,
|
|
437
|
-
}),
|
|
438
|
-
...(opts?.projectStatusOnCommercialRejection !== undefined && {
|
|
439
|
-
projectStatusOnCommercialRejection: opts.projectStatusOnCommercialRejection,
|
|
440
|
-
}),
|
|
441
|
-
...(opts?.projectInvoiceSettings !== undefined && {
|
|
442
|
-
projectInvoiceSettings: opts.projectInvoiceSettings,
|
|
443
|
-
}),
|
|
444
|
-
});
|
|
445
|
-
out(result);
|
|
446
|
-
}
|
|
447
|
-
const assertBudgetActionReadiness = async ({ budgetId, requireQuickbooksProject, actionLabel, }) => {
|
|
448
|
-
const budget = await api.budget.getBudget.query({ id: budgetId });
|
|
449
|
-
const hasNoItems = budget.budgetItems.length === 0;
|
|
450
|
-
if (hasNoItems) {
|
|
451
|
-
throw new Error(`Cannot ${actionLabel}: there are no line items in this budget.`);
|
|
452
|
-
}
|
|
453
|
-
const hasZeroCostItems = budget.budgetItems.some((item) => Number(item.cost ?? 0) === 0);
|
|
454
|
-
if (hasZeroCostItems) {
|
|
455
|
-
throw new Error(`Cannot ${actionLabel}: all line items must have a cost greater than zero.`);
|
|
456
|
-
}
|
|
457
|
-
const inactiveSuppliers = budget.budgetItems
|
|
458
|
-
.filter((item) => !item.supplier.active)
|
|
459
|
-
.map((item) => item.supplier.name);
|
|
460
|
-
const uniqueInactiveSuppliers = [...new Set(inactiveSuppliers)];
|
|
461
|
-
if (uniqueInactiveSuppliers.length > 0) {
|
|
462
|
-
throw new Error(`Cannot ${actionLabel}: inactive suppliers found (${uniqueInactiveSuppliers.join(", ")}).`);
|
|
463
|
-
}
|
|
464
|
-
if (requireQuickbooksProject && !budget.project.quickbooksProjectId) {
|
|
465
|
-
throw new Error("Cannot create estimate: project is not linked to QuickBooks yet.");
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const runBudgetPreflight = async (budgetId, preflight) => {
|
|
469
|
-
if (!preflight)
|
|
470
|
-
return;
|
|
471
|
-
if (preflight.company) {
|
|
472
|
-
await api.company.updateCompany.mutate(preflight.company);
|
|
473
|
-
}
|
|
474
|
-
if (preflight.contactPerson) {
|
|
475
|
-
await api.contactPerson.updateContactPerson.mutate(preflight.contactPerson);
|
|
476
|
-
}
|
|
477
|
-
if (preflight.eventDetails) {
|
|
478
|
-
const budget = await api.budget.getBudget.query({ id: budgetId });
|
|
479
|
-
const project = await api.project.getProjectById.query({
|
|
480
|
-
id: budget.projectId,
|
|
481
|
-
});
|
|
482
|
-
const day = new Date(preflight.eventDetails.date);
|
|
483
|
-
const start = new Date(preflight.eventDetails.startTime);
|
|
484
|
-
const end = new Date(preflight.eventDetails.endTime);
|
|
485
|
-
const payload = {
|
|
486
|
-
id: project.id,
|
|
487
|
-
name: project.name,
|
|
488
|
-
description: project.description ?? undefined,
|
|
489
|
-
dateRange: {
|
|
490
|
-
from: combineDateAndTime(day, start),
|
|
491
|
-
to: combineDateAndTime(day, end),
|
|
492
|
-
},
|
|
493
|
-
companyId: project.companyId,
|
|
494
|
-
contactPersonId: project.contactPersonId,
|
|
495
|
-
asanaTaskId: project.asanaTaskId ?? "",
|
|
496
|
-
insideSalesId: project.insideSalesId ?? "",
|
|
497
|
-
businessDevelopmentId: project.businessDevelopmentId ?? "",
|
|
498
|
-
venue: project.venue ?? "",
|
|
499
|
-
pax: formatOptionalProjectPaxInput(project.pax),
|
|
500
|
-
...(project.slackChannelId &&
|
|
501
|
-
project.slackChannelUrl &&
|
|
502
|
-
project.slackChannelName
|
|
503
|
-
? {
|
|
504
|
-
slackChannelId: project.slackChannelId,
|
|
505
|
-
slackChannelUrl: project.slackChannelUrl,
|
|
506
|
-
slackChannelName: project.slackChannelName,
|
|
507
|
-
}
|
|
508
|
-
: {}),
|
|
509
|
-
};
|
|
510
|
-
await api.project.updateProject.mutate(payload);
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
export async function listBills(opts) {
|
|
514
|
-
const input = {
|
|
515
|
-
projectId: opts.projectId,
|
|
516
|
-
budgetId: opts.budgetId,
|
|
517
|
-
statuses: billStatusesForApi(opts.statuses),
|
|
518
|
-
search: opts.search?.trim() || undefined,
|
|
519
|
-
isClaimable: opts.isClaimable,
|
|
520
|
-
createdByIds: opts.createdByIds && opts.createdByIds.length > 0
|
|
521
|
-
? opts.createdByIds
|
|
522
|
-
: undefined,
|
|
523
|
-
sortBy: opts.sortBy,
|
|
524
|
-
sortDir: opts.sortDir,
|
|
525
|
-
page: opts.page ?? 1,
|
|
526
|
-
pageSize: opts.pageSize ?? 20,
|
|
527
|
-
};
|
|
528
|
-
const result = await api.bill.getAll.query(input);
|
|
529
|
-
out(result);
|
|
530
|
-
}
|
|
531
|
-
export async function approveBill(billId) {
|
|
532
|
-
await assertSensitiveWorkflowConfirmed({
|
|
533
|
-
action: "Approve bill",
|
|
534
|
-
entity: `bill ${billId}`,
|
|
535
|
-
details: "and send requester reply email",
|
|
536
|
-
});
|
|
537
|
-
const notifications = await getAllNotifications();
|
|
538
|
-
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
539
|
-
n.type === BILL &&
|
|
540
|
-
(n.billId === billId || n.bill?.id === billId));
|
|
541
|
-
const approval = pending[0];
|
|
542
|
-
if (!approval) {
|
|
543
|
-
throw new Error("No pending bill approval found for this bill.");
|
|
544
|
-
}
|
|
545
|
-
const result = await api.bill.updateApproval.mutate({
|
|
546
|
-
billApprovalId: approval.id,
|
|
547
|
-
status: "APPROVED",
|
|
548
|
-
});
|
|
549
|
-
if (result.processing) {
|
|
550
|
-
out(result);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
let email;
|
|
554
|
-
try {
|
|
555
|
-
const delivery = result.emailDelivery;
|
|
556
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
557
|
-
}
|
|
558
|
-
catch (error) {
|
|
559
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
560
|
-
}
|
|
561
|
-
out({
|
|
562
|
-
...result,
|
|
563
|
-
email,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
export async function listApprovals(opts) {
|
|
567
|
-
const notifications = await getAllNotifications();
|
|
568
|
-
const pending = notifications.filter(isPendingRequest);
|
|
569
|
-
const typeFilter = opts.type === undefined || opts.type === "ALL"
|
|
570
|
-
? (_n) => true
|
|
571
|
-
: (n) => n.type === opts.type;
|
|
572
|
-
const filtered = pending
|
|
573
|
-
.filter(typeFilter)
|
|
574
|
-
.filter((n) => n.type !== BUDGET || !n.budget?.quickbooksEstimateId);
|
|
575
|
-
const items = filtered.map((n) => {
|
|
576
|
-
let id;
|
|
577
|
-
let name;
|
|
578
|
-
if (n.type === BUDGET && n.budget) {
|
|
579
|
-
id = n.budgetId ?? n.id;
|
|
580
|
-
name = n.budget.name;
|
|
581
|
-
}
|
|
582
|
-
else if (n.type === BILL && n.bill) {
|
|
583
|
-
id = n.bill.id;
|
|
584
|
-
name = `Bill ${n.bill.invoiceNumber ?? n.bill.id.slice(0, 8)}`;
|
|
585
|
-
}
|
|
586
|
-
else if (n.type === SUPPLIER && n.supplier) {
|
|
587
|
-
id = n.supplier.id;
|
|
588
|
-
name = n.supplier.name;
|
|
589
|
-
}
|
|
590
|
-
else if (n.type === QUOTATION && n.supplierQuotation) {
|
|
591
|
-
id = n.supplierQuotation.id;
|
|
592
|
-
name = `Quotation ${n.supplierQuotation.supplier.name}`;
|
|
593
|
-
}
|
|
594
|
-
else if (n.type === CUSTOMER_INVOICE && n.customerInvoiceBatch) {
|
|
595
|
-
id = n.customerInvoiceBatch.id;
|
|
596
|
-
name = `Customer invoice ${n.customerInvoiceBatch.project.name}`;
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
id = n.id;
|
|
600
|
-
name = "Unknown";
|
|
601
|
-
}
|
|
602
|
-
return {
|
|
603
|
-
approvalId: n.id,
|
|
604
|
-
type: n.type,
|
|
605
|
-
id,
|
|
606
|
-
name,
|
|
607
|
-
requestedBy: n.requester?.name ?? n.requester?.email ?? "Unknown",
|
|
608
|
-
requestedAt: typeof n.createdAt === "string"
|
|
609
|
-
? n.createdAt
|
|
610
|
-
: n.createdAt instanceof Date
|
|
611
|
-
? n.createdAt.toISOString()
|
|
612
|
-
: String(n.createdAt),
|
|
613
|
-
};
|
|
614
|
-
});
|
|
615
|
-
out({ items });
|
|
616
|
-
}
|
|
617
|
-
export async function listSuppliers(opts) {
|
|
618
|
-
const result = await api.supplier.getSuppliers.query({
|
|
619
|
-
page: opts.page ?? 1,
|
|
620
|
-
perPage: opts.perPage ?? 10,
|
|
621
|
-
sort: opts.sort ?? [{ id: "createdAt", desc: true }],
|
|
622
|
-
name: opts.name,
|
|
623
|
-
createdBy: opts.createdBy,
|
|
624
|
-
gstRegistered: opts.gstRegistered ?? null,
|
|
625
|
-
status: opts.status,
|
|
626
|
-
supplierTags: opts.supplierTags,
|
|
627
|
-
active: opts.active ?? true,
|
|
628
|
-
});
|
|
629
|
-
out(result);
|
|
630
|
-
}
|
|
631
|
-
// --- Bills (MCP parity) ---
|
|
632
|
-
export async function createBillApproval(billId) {
|
|
633
|
-
await assertSensitiveWorkflowConfirmed({
|
|
634
|
-
action: "Create bill approval request",
|
|
635
|
-
entity: `bill ${billId}`,
|
|
636
|
-
details: "and send approval request emails",
|
|
637
|
-
});
|
|
638
|
-
const result = await api.bill.createApproval.mutate({ billId });
|
|
639
|
-
const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
|
|
640
|
-
let email;
|
|
641
|
-
if (approvalIds.length === 0) {
|
|
642
|
-
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
try {
|
|
646
|
-
const delivery = result.emailDelivery;
|
|
647
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
out({
|
|
654
|
-
...result,
|
|
655
|
-
email,
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
export const validateBillStatusUpdateOptions = (opts) => {
|
|
659
|
-
if (opts.status === BillStatus.PAID && !opts.paymentReference?.trim()) {
|
|
660
|
-
throw new Error("update-bill-status requires --paymentReference when status is PAID.");
|
|
661
|
-
}
|
|
662
|
-
if (opts.status !== BillStatus.PAID && opts.paymentProofPath !== undefined) {
|
|
663
|
-
throw new Error("--paymentProof can only be used when status is PAID.");
|
|
664
|
-
}
|
|
665
|
-
if (opts.paymentProofPath !== undefined && !opts.paymentProofPath.trim()) {
|
|
666
|
-
throw new Error("--paymentProof requires a PDF file path.");
|
|
667
|
-
}
|
|
668
|
-
};
|
|
669
|
-
const deleteStagedPaymentProof = async (projectId, key) => {
|
|
670
|
-
const cleanup = await api.attachment.deleteStagedBillAttachments.mutate({
|
|
671
|
-
projectId,
|
|
672
|
-
keys: [key],
|
|
673
|
-
});
|
|
674
|
-
if (cleanup.failedKeys.includes(key)) {
|
|
675
|
-
throw new Error(`Failed to delete staged payment proof ${key}.`);
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
export const rethrowAfterStagedPaymentProofCleanup = async (error, cleanup) => {
|
|
679
|
-
if (cleanup) {
|
|
680
|
-
try {
|
|
681
|
-
await cleanup();
|
|
682
|
-
}
|
|
683
|
-
catch (cleanupError) {
|
|
684
|
-
throw new AggregateError([error, cleanupError], "Bill status update failed and its staged payment proof could not be cleaned up.");
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
throw error;
|
|
688
|
-
};
|
|
689
|
-
const stageBillPaymentProof = async (projectId, filePath) => {
|
|
690
|
-
const buffer = await readFile(filePath);
|
|
691
|
-
const fileName = basename(filePath);
|
|
692
|
-
const size = buffer.byteLength;
|
|
693
|
-
const contentType = billAttachmentContentTypeForFileName(fileName);
|
|
694
|
-
if (!fileName.toLowerCase().endsWith(".pdf") ||
|
|
695
|
-
contentType !== "application/pdf") {
|
|
696
|
-
throw new Error("Payment proof must be a PDF.");
|
|
697
|
-
}
|
|
698
|
-
if (size === 0) {
|
|
699
|
-
throw new Error("Payment proof must not be empty.");
|
|
700
|
-
}
|
|
701
|
-
if (size > PAYMENT_PROOF_MAX_SIZE) {
|
|
702
|
-
throw new Error("Payment proof must be 20MB or smaller.");
|
|
703
|
-
}
|
|
704
|
-
const { uploadUrl, key } = await api.attachment.requestStagedBillAttachmentUpload.mutate({
|
|
705
|
-
projectId,
|
|
706
|
-
fileName,
|
|
707
|
-
size,
|
|
708
|
-
contentType,
|
|
709
|
-
});
|
|
710
|
-
try {
|
|
711
|
-
const response = await fetch(uploadUrl, {
|
|
712
|
-
method: "PUT",
|
|
713
|
-
body: buffer,
|
|
714
|
-
headers: { "Content-Type": contentType },
|
|
715
|
-
});
|
|
716
|
-
if (!response.ok) {
|
|
717
|
-
throw new Error(`S3 upload failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
catch (uploadError) {
|
|
721
|
-
try {
|
|
722
|
-
await deleteStagedPaymentProof(projectId, key);
|
|
723
|
-
}
|
|
724
|
-
catch (cleanupError) {
|
|
725
|
-
throw new AggregateError([uploadError, cleanupError], "Payment-proof upload failed and its staged object could not be cleaned up.");
|
|
726
|
-
}
|
|
727
|
-
throw uploadError;
|
|
728
|
-
}
|
|
729
|
-
return { id: randomUUID(), key, name: fileName, size };
|
|
730
|
-
};
|
|
731
|
-
export async function updateBillStatus(opts) {
|
|
732
|
-
validateBillStatusUpdateOptions(opts);
|
|
733
|
-
await assertSensitiveWorkflowConfirmed({
|
|
734
|
-
action: "Update bill status",
|
|
735
|
-
entity: `bill ${opts.id}`,
|
|
736
|
-
details: `to ${opts.status}`,
|
|
737
|
-
});
|
|
738
|
-
const bill = opts.paymentProofPath
|
|
739
|
-
? await api.bill.getById.query({ id: opts.id })
|
|
740
|
-
: null;
|
|
741
|
-
const paymentProof = bill && opts.paymentProofPath
|
|
742
|
-
? await stageBillPaymentProof(bill.projectId, opts.paymentProofPath)
|
|
743
|
-
: undefined;
|
|
744
|
-
try {
|
|
745
|
-
const result = await api.bill.updateStatus.mutate({
|
|
746
|
-
id: opts.id,
|
|
747
|
-
status: opts.status,
|
|
748
|
-
rejectionReason: opts.rejectionReason,
|
|
749
|
-
paymentTrackingUrl: opts.paymentTrackingUrl,
|
|
750
|
-
paymentReference: opts.paymentReference,
|
|
751
|
-
paymentProofAttachments: paymentProof ? [paymentProof] : undefined,
|
|
752
|
-
});
|
|
753
|
-
out(result);
|
|
754
|
-
}
|
|
755
|
-
catch (error) {
|
|
756
|
-
await rethrowAfterStagedPaymentProofCleanup(error, bill && paymentProof
|
|
757
|
-
? async () => {
|
|
758
|
-
// The cleanup endpoint checks the staged-upload record and refuses to
|
|
759
|
-
// delete a key that an ambiguously successful update already consumed.
|
|
760
|
-
await deleteStagedPaymentProof(bill.projectId, paymentProof.key);
|
|
761
|
-
}
|
|
762
|
-
: undefined);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
export async function patchBillPayment(opts) {
|
|
766
|
-
const input = { id: opts.id };
|
|
767
|
-
if (opts.paymentTrackingUrl !== undefined)
|
|
768
|
-
input.paymentTrackingUrl = opts.paymentTrackingUrl;
|
|
769
|
-
if (opts.paymentReference !== undefined)
|
|
770
|
-
input.paymentReference = opts.paymentReference;
|
|
771
|
-
if (opts.quickbooksBillId !== undefined)
|
|
772
|
-
input.quickbooksBillId = opts.quickbooksBillId;
|
|
773
|
-
if (opts.paymentDate !== undefined) {
|
|
774
|
-
input.paymentDate =
|
|
775
|
-
opts.paymentDate === null
|
|
776
|
-
? null
|
|
777
|
-
: typeof opts.paymentDate === "string"
|
|
778
|
-
? new Date(opts.paymentDate)
|
|
779
|
-
: opts.paymentDate;
|
|
780
|
-
}
|
|
781
|
-
const result = await api.bill.patchPayment.mutate(input);
|
|
782
|
-
out(result);
|
|
783
|
-
}
|
|
784
|
-
export async function patchBillInvoiceNumber(id, invoiceNumber) {
|
|
785
|
-
const result = await api.bill.patchInvoiceNumber.mutate({
|
|
786
|
-
id,
|
|
787
|
-
invoiceNumber,
|
|
788
|
-
});
|
|
789
|
-
out(result);
|
|
790
|
-
}
|
|
791
|
-
export async function getBillAttachments(id) {
|
|
792
|
-
const result = await api.bill.getAttachments.query({ id });
|
|
793
|
-
out(result);
|
|
794
|
-
}
|
|
795
|
-
export async function stageBillAttachmentsFromPaths(projectId, filePaths) {
|
|
796
|
-
const attachments = [];
|
|
797
|
-
for (const filePath of filePaths) {
|
|
798
|
-
const buffer = await readFile(filePath);
|
|
799
|
-
const fileName = basename(filePath);
|
|
800
|
-
const size = buffer.byteLength;
|
|
801
|
-
const contentType = billAttachmentContentTypeForFileName(fileName);
|
|
802
|
-
const { uploadUrl, key } = await api.attachment.requestStagedBillAttachmentUpload.mutate({
|
|
803
|
-
projectId,
|
|
804
|
-
fileName,
|
|
805
|
-
size,
|
|
806
|
-
contentType,
|
|
807
|
-
});
|
|
808
|
-
const response = await fetch(uploadUrl, {
|
|
809
|
-
method: "PUT",
|
|
810
|
-
body: buffer,
|
|
811
|
-
headers: { "Content-Type": contentType },
|
|
812
|
-
});
|
|
813
|
-
if (!response.ok) {
|
|
814
|
-
throw new Error(`S3 upload failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
|
|
815
|
-
}
|
|
816
|
-
attachments.push({ id: randomUUID(), key, name: fileName, size });
|
|
817
|
-
}
|
|
818
|
-
out({
|
|
819
|
-
success: true,
|
|
820
|
-
projectId,
|
|
821
|
-
count: attachments.length,
|
|
822
|
-
attachments,
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
export const cleanupStagedBillAttachments = async (projectId, keys) => {
|
|
826
|
-
const result = await api.attachment.deleteStagedBillAttachments.mutate({
|
|
827
|
-
projectId,
|
|
828
|
-
keys,
|
|
829
|
-
});
|
|
830
|
-
out(result);
|
|
831
|
-
};
|
|
832
|
-
export async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
|
|
833
|
-
const confirmed = await uploadSingleBillAttachmentFromPath(billId, filePath, type);
|
|
834
|
-
out(confirmed);
|
|
835
|
-
}
|
|
836
|
-
const uploadSingleBillAttachmentFromPath = async (billId, filePath, type = "BILL") => {
|
|
837
|
-
const buf = await readFile(filePath);
|
|
838
|
-
const fileName = basename(filePath);
|
|
839
|
-
const size = buf.byteLength;
|
|
840
|
-
const attachmentContentType = billAttachmentContentTypeForFileName(fileName);
|
|
841
|
-
const { uploadUrl, key } = await api.attachment.requestBillAttachmentUpload.mutate({
|
|
842
|
-
billId,
|
|
843
|
-
contentType: attachmentContentType,
|
|
844
|
-
fileName,
|
|
845
|
-
size,
|
|
846
|
-
});
|
|
847
|
-
const res = await fetch(uploadUrl, {
|
|
848
|
-
method: "PUT",
|
|
849
|
-
body: buf,
|
|
850
|
-
headers: { "Content-Type": attachmentContentType },
|
|
851
|
-
});
|
|
852
|
-
if (!res.ok) {
|
|
853
|
-
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
854
|
-
}
|
|
855
|
-
return await api.attachment.confirmBillAttachment.mutate({
|
|
856
|
-
billId,
|
|
857
|
-
key,
|
|
858
|
-
name: fileName,
|
|
859
|
-
size,
|
|
860
|
-
type,
|
|
861
|
-
});
|
|
862
|
-
};
|
|
863
|
-
export async function uploadBillAttachmentsFromPaths(billId, filePaths, type = "BILL") {
|
|
864
|
-
if (filePaths.length === 0) {
|
|
865
|
-
throw new Error("At least one bill attachment file path is required");
|
|
866
|
-
}
|
|
867
|
-
const results = [];
|
|
868
|
-
for (const filePath of filePaths) {
|
|
869
|
-
results.push(await uploadSingleBillAttachmentFromPath(billId, filePath, type));
|
|
870
|
-
}
|
|
871
|
-
out({
|
|
872
|
-
success: true,
|
|
873
|
-
billId,
|
|
874
|
-
type,
|
|
875
|
-
count: results.length,
|
|
876
|
-
attachments: results.map((result) => result.attachment),
|
|
877
|
-
results,
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
export async function uploadBillDocumentsFromPaths({ billId, invoicePaths = [], paymentProofPaths = [], }) {
|
|
881
|
-
if (invoicePaths.length === 0 && paymentProofPaths.length === 0) {
|
|
882
|
-
throw new Error("At least one invoice or payment proof file path is required");
|
|
883
|
-
}
|
|
884
|
-
const invoiceResults = [];
|
|
885
|
-
for (const filePath of invoicePaths) {
|
|
886
|
-
invoiceResults.push(await uploadSingleBillAttachmentFromPath(billId, filePath, "BILL"));
|
|
887
|
-
}
|
|
888
|
-
const paymentProofResults = [];
|
|
889
|
-
for (const filePath of paymentProofPaths) {
|
|
890
|
-
paymentProofResults.push(await uploadSingleBillAttachmentFromPath(billId, filePath, "BILL_PAYMENT_PROOF"));
|
|
891
|
-
}
|
|
892
|
-
out({
|
|
893
|
-
success: true,
|
|
894
|
-
billId,
|
|
895
|
-
count: invoiceResults.length + paymentProofResults.length,
|
|
896
|
-
invoiceAttachments: invoiceResults.map((result) => result.attachment),
|
|
897
|
-
paymentProofAttachments: paymentProofResults.map((result) => result.attachment),
|
|
898
|
-
results: [...invoiceResults, ...paymentProofResults],
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
export async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
902
|
-
const buf = await readFile(filePath);
|
|
903
|
-
const fileName = basename(filePath);
|
|
904
|
-
if (buf.byteLength > QUOTATION_ATTACHMENT_MAX_SIZE) {
|
|
905
|
-
throw new Error("Quotation attachment must be 20MB or smaller.");
|
|
906
|
-
}
|
|
907
|
-
const attachmentContentType = quotationAttachmentContentTypeForFileName(fileName);
|
|
908
|
-
const ext = fileName
|
|
909
|
-
.split(".")
|
|
910
|
-
.pop()
|
|
911
|
-
?.toLowerCase()
|
|
912
|
-
.replace(/[^a-z0-9]/g, "");
|
|
913
|
-
const key = `quotations/${projectId}/${randomUUID()}.${ext || "pdf"}`;
|
|
914
|
-
const uploadUrl = await api.attachment.getPresignedUrlToUpload.mutate({
|
|
915
|
-
key,
|
|
916
|
-
size: buf.byteLength,
|
|
917
|
-
contentType: attachmentContentType,
|
|
918
|
-
});
|
|
919
|
-
const res = await fetch(uploadUrl, {
|
|
920
|
-
method: "PUT",
|
|
921
|
-
body: buf,
|
|
922
|
-
headers: { "Content-Type": attachmentContentType },
|
|
923
|
-
});
|
|
924
|
-
if (!res.ok) {
|
|
925
|
-
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
926
|
-
}
|
|
927
|
-
out({
|
|
928
|
-
id: randomUUID(),
|
|
929
|
-
name: fileName,
|
|
930
|
-
key,
|
|
931
|
-
size: buf.byteLength,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
export const cleanupStagedQuotationAttachments = async (projectId, keys) => {
|
|
935
|
-
const result = await api.quotation.deleteStagedAttachments.mutate({
|
|
936
|
-
projectId,
|
|
937
|
-
keys,
|
|
938
|
-
});
|
|
939
|
-
out(result);
|
|
940
|
-
};
|
|
941
|
-
export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
942
|
-
await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
|
|
943
|
-
const buf = await readFile(filePath);
|
|
944
|
-
const fileName = basename(filePath);
|
|
945
|
-
const size = buf.byteLength;
|
|
946
|
-
const attachmentContentType = attachmentContentTypeForFileName(fileName);
|
|
947
|
-
const { uploadUrl, key } = await api.attachment.requestBudgetAttachmentUpload.mutate({
|
|
948
|
-
budgetId,
|
|
949
|
-
contentType: attachmentContentType,
|
|
950
|
-
fileName,
|
|
951
|
-
size,
|
|
952
|
-
});
|
|
953
|
-
const res = await fetch(uploadUrl, {
|
|
954
|
-
method: "PUT",
|
|
955
|
-
body: buf,
|
|
956
|
-
headers: { "Content-Type": attachmentContentType },
|
|
957
|
-
});
|
|
958
|
-
if (!res.ok) {
|
|
959
|
-
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
960
|
-
}
|
|
961
|
-
const confirmed = await api.attachment.confirmBudgetAttachment.mutate({
|
|
962
|
-
budgetId,
|
|
963
|
-
key,
|
|
964
|
-
name: fileName,
|
|
965
|
-
size,
|
|
966
|
-
});
|
|
967
|
-
out(confirmed);
|
|
968
|
-
}
|
|
969
|
-
export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
970
|
-
await assertSensitiveWorkflowConfirmed({
|
|
971
|
-
action: "Mark budget won",
|
|
972
|
-
entity: `budget ${budgetId}`,
|
|
973
|
-
details: "by uploading proof and setting status to ESTIMATE_ACCEPTED",
|
|
974
|
-
});
|
|
975
|
-
await confirmLockedBudgetChangeByBudgetId(budgetId, "Mark budget won");
|
|
976
|
-
const buf = await readFile(filePath);
|
|
977
|
-
const fileName = basename(filePath);
|
|
978
|
-
const size = buf.byteLength;
|
|
979
|
-
const attachmentContentType = attachmentContentTypeForFileName(fileName);
|
|
980
|
-
const { uploadUrl, key } = await api.attachment.requestBudgetWinProofUpload.mutate({
|
|
981
|
-
budgetId,
|
|
982
|
-
contentType: attachmentContentType,
|
|
983
|
-
fileName,
|
|
984
|
-
size,
|
|
985
|
-
});
|
|
986
|
-
const res = await fetch(uploadUrl, {
|
|
987
|
-
method: "PUT",
|
|
988
|
-
body: buf,
|
|
989
|
-
headers: { "Content-Type": attachmentContentType },
|
|
990
|
-
});
|
|
991
|
-
if (!res.ok) {
|
|
992
|
-
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
993
|
-
}
|
|
994
|
-
await api.attachment.confirmBudgetWinProofAttachment.mutate({
|
|
995
|
-
budgetId,
|
|
996
|
-
key,
|
|
997
|
-
name: fileName,
|
|
998
|
-
size,
|
|
999
|
-
});
|
|
1000
|
-
const result = await api.budget.updateBudgetStatus.mutate({
|
|
1001
|
-
id: budgetId,
|
|
1002
|
-
status: "ESTIMATE_ACCEPTED",
|
|
1003
|
-
...(opts?.projectManagerId !== undefined && {
|
|
1004
|
-
projectManagerId: opts.projectManagerId,
|
|
1005
|
-
}),
|
|
1006
|
-
...(opts?.markProjectWon !== undefined && {
|
|
1007
|
-
markProjectWon: opts.markProjectWon,
|
|
1008
|
-
}),
|
|
1009
|
-
...(opts?.projectInvoiceSettings !== undefined && {
|
|
1010
|
-
projectInvoiceSettings: opts.projectInvoiceSettings,
|
|
1011
|
-
}),
|
|
1012
|
-
});
|
|
1013
|
-
out(result);
|
|
1014
|
-
}
|
|
1015
|
-
export async function markBudgetWonWithProof(budgetId, filePath, opts) {
|
|
1016
|
-
await uploadBudgetWinProofFromPath(budgetId, filePath, opts);
|
|
1017
|
-
}
|
|
1018
|
-
export async function getBillDetails(id) {
|
|
1019
|
-
const result = await api.bill.getById.query({ id });
|
|
1020
|
-
out(result);
|
|
1021
|
-
}
|
|
1022
|
-
// --- Budgets (MCP parity) ---
|
|
1023
|
-
export async function getBudgetItemsOnly(budgetId) {
|
|
1024
|
-
const items = await api.budget.getBudgetItems.query({ budgetId });
|
|
1025
|
-
out(items);
|
|
1026
|
-
}
|
|
1027
|
-
export async function addBudgetItems(opts) {
|
|
1028
|
-
await confirmLockedBudgetChangeByBudgetId(opts.budgetId, "Add budget items");
|
|
1029
|
-
const result = await api.budgetItem.addBudgetItems.mutate(opts);
|
|
1030
|
-
out(result);
|
|
1031
|
-
}
|
|
1032
|
-
export async function updateBudgetItem(input) {
|
|
1033
|
-
await confirmLockedBudgetChangeByBudgetItemId(input.id, "Update budget item");
|
|
1034
|
-
const result = await api.budgetItem.updateBudgetItem.mutate({
|
|
1035
|
-
...input,
|
|
1036
|
-
...(input.description !== undefined &&
|
|
1037
|
-
input.descriptionRichText === undefined && {
|
|
1038
|
-
descriptionRichText: createRichTextFromPlainText(input.description),
|
|
1039
|
-
}),
|
|
1040
|
-
...(input.note !== undefined &&
|
|
1041
|
-
input.noteRichText === undefined && {
|
|
1042
|
-
noteRichText: createRichTextFromPlainText(input.note),
|
|
1043
|
-
}),
|
|
1044
|
-
});
|
|
1045
|
-
out(result);
|
|
1046
|
-
}
|
|
1047
|
-
export async function removeBudgetItem(budgetItemId) {
|
|
1048
|
-
await confirmLockedBudgetChangeByBudgetItemId(budgetItemId, "Remove budget item");
|
|
1049
|
-
const result = await api.budgetItem.removeBudgetItem.mutate({
|
|
1050
|
-
budgetItemId,
|
|
1051
|
-
});
|
|
1052
|
-
out(result);
|
|
1053
|
-
}
|
|
1054
|
-
export const replaceBudgetItem = async (input) => {
|
|
1055
|
-
await confirmLockedBudgetChangeByBudgetItemId(input.budgetItemId, "Replace unavailable budget item");
|
|
1056
|
-
const result = await api.budgetItem.replaceBudgetItem.mutate(input);
|
|
1057
|
-
out(result);
|
|
1058
|
-
};
|
|
1059
|
-
export const approveUnavailableItemException = async (input) => {
|
|
1060
|
-
await confirmLockedBudgetChangeByBudgetItemId(input.budgetItemId, "Approve unavailable item exception");
|
|
1061
|
-
const result = await api.budgetItem.approveUnavailableItemException.mutate(input);
|
|
1062
|
-
out(result);
|
|
1063
|
-
};
|
|
1064
|
-
export async function getBudgetCategories() {
|
|
1065
|
-
const categories = await api.budget.getBudgetCategories.query();
|
|
1066
|
-
out(categories);
|
|
1067
|
-
}
|
|
1068
|
-
export async function getBudgetVersions(budgetId) {
|
|
1069
|
-
const versions = await api.budgetVersion.getBudgetVersions.query({
|
|
1070
|
-
budgetId,
|
|
1071
|
-
});
|
|
1072
|
-
out(versions);
|
|
1073
|
-
}
|
|
1074
|
-
export async function createBudgetApproval(budgetId, preflight) {
|
|
1075
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1076
|
-
action: "Create budget approval request",
|
|
1077
|
-
entity: `budget ${budgetId}`,
|
|
1078
|
-
details: "and send approval request emails",
|
|
1079
|
-
});
|
|
1080
|
-
await confirmLockedBudgetChangeByBudgetId(budgetId, "Create budget approval");
|
|
1081
|
-
await runBudgetPreflight(budgetId, preflight);
|
|
1082
|
-
await assertBudgetActionReadiness({
|
|
1083
|
-
budgetId,
|
|
1084
|
-
requireQuickbooksProject: false,
|
|
1085
|
-
actionLabel: "request approval",
|
|
1086
|
-
});
|
|
1087
|
-
const result = await api.budget.createBudgetApproval.mutate({ budgetId });
|
|
1088
|
-
const approvalIds = (result.results ?? []).map((item) => ({ id: item.id }));
|
|
1089
|
-
let email;
|
|
1090
|
-
if (approvalIds.length === 0) {
|
|
1091
|
-
email = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
1092
|
-
}
|
|
1093
|
-
else {
|
|
1094
|
-
try {
|
|
1095
|
-
const delivery = result.emailDelivery;
|
|
1096
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
out({
|
|
1103
|
-
...result,
|
|
1104
|
-
email,
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
export async function createEstimate(budgetId, preflight) {
|
|
1108
|
-
await confirmLockedBudgetChangeByBudgetId(budgetId, "Create estimate");
|
|
1109
|
-
await runBudgetPreflight(budgetId, preflight);
|
|
1110
|
-
await assertBudgetActionReadiness({
|
|
1111
|
-
budgetId,
|
|
1112
|
-
requireQuickbooksProject: true,
|
|
1113
|
-
actionLabel: "create estimate",
|
|
1114
|
-
});
|
|
1115
|
-
const result = await api.quickbooks.createEstimate.mutate({ budgetId });
|
|
1116
|
-
out(result);
|
|
1117
|
-
}
|
|
1118
|
-
export const describeEstimateEmailConfirmation = (input) => `to ${input.to}; CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; queue durable worker delivery with the QBO estimate PDF, terms and conditions, and Budget Builder attachments; mark the estimate sent after provider acceptance`;
|
|
1119
|
-
export const sendEstimateToContactPersonFromPayload = async (raw) => {
|
|
1120
|
-
const input = parseSendEstimateToContactPersonPayload(raw);
|
|
1121
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1122
|
-
action: "Send estimate email",
|
|
1123
|
-
entity: `budget ${input.budgetId}`,
|
|
1124
|
-
details: describeEstimateEmailConfirmation(input),
|
|
1125
|
-
});
|
|
1126
|
-
const result = await api.email.sendEstimateToContactPerson.mutate(input);
|
|
1127
|
-
out(result);
|
|
1128
|
-
};
|
|
1129
|
-
export async function getBudgetDetails(budgetId) {
|
|
1130
|
-
const detail = await api.budget.getBudgetDetail.query({ id: budgetId });
|
|
1131
|
-
out(detail);
|
|
1132
|
-
}
|
|
1133
|
-
export async function createBudgetFromPayload(raw) {
|
|
1134
|
-
const input = parseCreateBudgetPayload(raw);
|
|
1135
|
-
const result = await api.budget.createBudget.mutate(input);
|
|
1136
|
-
out(result);
|
|
1137
|
-
}
|
|
1138
|
-
export async function updateBudgetFromPayload(raw) {
|
|
1139
|
-
const input = parseUpdateBudgetPayload(raw);
|
|
1140
|
-
await confirmLockedBudgetChangeByBudgetId(input.id, "Update budget");
|
|
1141
|
-
const result = await api.budget.updateBudget.mutate(input);
|
|
1142
|
-
out(result);
|
|
1143
|
-
}
|
|
1144
|
-
export async function deleteBudgetById(budgetId) {
|
|
1145
|
-
await confirmLockedBudgetChangeByBudgetId(budgetId, "Delete budget");
|
|
1146
|
-
const result = await api.budget.deleteBudget.mutate({ id: budgetId });
|
|
1147
|
-
out(result);
|
|
1148
|
-
}
|
|
1149
|
-
export async function deleteProjectById(projectId) {
|
|
1150
|
-
const result = await api.project.deleteProject.mutate({ id: projectId });
|
|
1151
|
-
out(result);
|
|
1152
|
-
}
|
|
1153
|
-
export async function deleteCompanyById(companyId) {
|
|
1154
|
-
const result = await api.company.deleteCompany.mutate({ id: companyId });
|
|
1155
|
-
out(result);
|
|
1156
|
-
}
|
|
1157
|
-
export async function reorderBudgetItemsCli(opts) {
|
|
1158
|
-
await confirmLockedBudgetChangeByBudgetId(opts.budgetId, "Reorder budget items");
|
|
1159
|
-
const result = await api.budgetItem.reorderBudgetItems.mutate(opts);
|
|
1160
|
-
out(result);
|
|
1161
|
-
}
|
|
1162
|
-
export async function updateBudgetItemSupplierCli(opts) {
|
|
1163
|
-
await confirmLockedBudgetChangeByBudgetItemId(opts.budgetItemId, "Update budget item supplier");
|
|
1164
|
-
const result = await api.budgetItem.updateBudgetItemSupplier.mutate(opts);
|
|
1165
|
-
out(result);
|
|
1166
|
-
}
|
|
1167
|
-
export async function createBudgetCategory(name) {
|
|
1168
|
-
const result = await api.budget.createBudgetCategory.mutate({ name });
|
|
1169
|
-
out(result);
|
|
1170
|
-
}
|
|
1171
|
-
export async function updateBudgetCategory(opts) {
|
|
1172
|
-
const result = await api.budget.updateBudgetCategory.mutate(opts);
|
|
1173
|
-
out(result);
|
|
1174
|
-
}
|
|
1175
|
-
export async function deleteBudgetCategory(id) {
|
|
1176
|
-
const result = await api.budget.deleteBudgetCategory.mutate({ id });
|
|
1177
|
-
out(result);
|
|
1178
|
-
}
|
|
1179
|
-
export async function updateBudgetCommissionFromPayload(raw) {
|
|
1180
|
-
const input = parseUpdateBudgetCommissionPayload(raw);
|
|
1181
|
-
await confirmLockedBudgetChangeByBudgetId(input.budgetId, "Update budget commission");
|
|
1182
|
-
const result = await api.budget.updateBudgetCommission.mutate(input);
|
|
1183
|
-
out(result);
|
|
1184
|
-
}
|
|
1185
|
-
export async function updateBudgetDiscountFromPayload(raw) {
|
|
1186
|
-
const input = parseBudgetDiscountPayload(raw);
|
|
1187
|
-
await confirmLockedBudgetChangeByBudgetId(input.budgetId, "Update budget discount");
|
|
1188
|
-
const result = await api.budget.updateBudgetDiscount.mutate(input);
|
|
1189
|
-
out(result);
|
|
1190
|
-
}
|
|
1191
|
-
export const deleteBudgetDiscount = async (budgetId) => {
|
|
1192
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1193
|
-
action: "Remove budget discount",
|
|
1194
|
-
entity: `budget ${budgetId}`,
|
|
1195
|
-
details: "and return the budget to DRAFT",
|
|
1196
|
-
});
|
|
1197
|
-
const result = await api.budget.deleteBudgetDiscount.mutate({ budgetId });
|
|
1198
|
-
out(result);
|
|
1199
|
-
};
|
|
1200
|
-
export const deleteBudgetCommission = async (budgetId) => {
|
|
1201
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1202
|
-
action: "Remove budget commission",
|
|
1203
|
-
entity: `budget ${budgetId}`,
|
|
1204
|
-
details: "and return the budget to DRAFT",
|
|
1205
|
-
});
|
|
1206
|
-
const result = await api.budget.deleteBudgetCommission.mutate({ budgetId });
|
|
1207
|
-
out(result);
|
|
1208
|
-
};
|
|
1209
|
-
export const setBudgetItemsNotUtilized = async (budgetItemIds, notUtilized) => {
|
|
1210
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1211
|
-
action: notUtilized
|
|
1212
|
-
? "Mark budget lines Not Utilized"
|
|
1213
|
-
: "Restore budget line",
|
|
1214
|
-
entity: `budget item${budgetItemIds.length === 1 ? "" : "s"} ${budgetItemIds.join(", ")}`,
|
|
1215
|
-
details: notUtilized
|
|
1216
|
-
? "and remove linked QuickBooks placeholder expenses"
|
|
1217
|
-
: "and restore its QuickBooks placeholder expense when required",
|
|
1218
|
-
});
|
|
1219
|
-
const result = await api.budget.setBudgetItemsNotUtilized.mutate({
|
|
1220
|
-
budgetItemIds,
|
|
1221
|
-
notUtilized,
|
|
1222
|
-
});
|
|
1223
|
-
out(result);
|
|
1224
|
-
};
|
|
1225
|
-
export const createPlaceholderBillForBudgetItem = async (budgetItemId) => {
|
|
1226
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1227
|
-
action: "Create QuickBooks placeholder bill",
|
|
1228
|
-
entity: `budget item ${budgetItemId}`,
|
|
1229
|
-
});
|
|
1230
|
-
const result = await api.budget.createPlaceholderBillForBudgetItem.mutate({
|
|
1231
|
-
budgetItemId,
|
|
1232
|
-
});
|
|
1233
|
-
out(result);
|
|
1234
|
-
};
|
|
1235
|
-
export const renameBudgetVersion = async (versionId, newName) => {
|
|
1236
|
-
const result = await api.budgetVersion.renameBudgetVersion.mutate({
|
|
1237
|
-
versionId,
|
|
1238
|
-
newName,
|
|
1239
|
-
});
|
|
1240
|
-
out(result);
|
|
1241
|
-
};
|
|
1242
|
-
export const restoreBudgetVersion = async (versionId) => {
|
|
1243
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1244
|
-
action: "Restore budget version",
|
|
1245
|
-
entity: `budget version ${versionId}`,
|
|
1246
|
-
details: "replace the current budget, project fields, items, and attachments, clear its QuickBooks estimate link, and return it to DRAFT",
|
|
1247
|
-
});
|
|
1248
|
-
const result = await api.budgetVersion.restoreBudgetVersion.mutate({
|
|
1249
|
-
versionId,
|
|
1250
|
-
});
|
|
1251
|
-
out(result);
|
|
1252
|
-
};
|
|
1253
|
-
export async function deleteItemById(itemId) {
|
|
1254
|
-
const result = await api.item.deleteItem.mutate({ id: itemId });
|
|
1255
|
-
out(result);
|
|
1256
|
-
}
|
|
1257
|
-
export async function archiveItemsByIds(ids) {
|
|
1258
|
-
const result = await api.item.archiveItems.mutate({ ids });
|
|
1259
|
-
out(result);
|
|
1260
|
-
}
|
|
1261
|
-
export async function deleteSuppliersByIds(ids) {
|
|
1262
|
-
const result = await api.supplier.deleteSuppliers.mutate({ ids });
|
|
1263
|
-
out(result);
|
|
1264
|
-
}
|
|
1265
|
-
export async function reactivateSuppliersByIds(ids) {
|
|
1266
|
-
const result = await api.supplier.reactivateSuppliers.mutate({ ids });
|
|
1267
|
-
out(result);
|
|
1268
|
-
}
|
|
1269
|
-
export async function createSupplierCertification(name) {
|
|
1270
|
-
const result = await api.supplier.createCertification.mutate({ name });
|
|
1271
|
-
out(result);
|
|
1272
|
-
}
|
|
1273
|
-
export async function createSupplierPaymentMethod(name) {
|
|
1274
|
-
const result = await api.supplier.createPaymentMethod.mutate({ name });
|
|
1275
|
-
out(result);
|
|
1276
|
-
}
|
|
1277
|
-
export async function createSupplierRoleOption(name) {
|
|
1278
|
-
const result = await api.supplier.createSupplierRole.mutate({ name });
|
|
1279
|
-
out(result);
|
|
1280
|
-
}
|
|
1281
|
-
export async function createSupplierTagOption(name) {
|
|
1282
|
-
const result = await api.supplier.createSupplierTag.mutate({ name });
|
|
1283
|
-
out(result);
|
|
1284
|
-
}
|
|
1285
|
-
export async function createItemCategory(name) {
|
|
1286
|
-
const result = await api.itemCategory.createItemCategory.mutate({ name });
|
|
1287
|
-
out(result);
|
|
1288
|
-
}
|
|
1289
|
-
export async function updateItemCategory(opts) {
|
|
1290
|
-
const result = await api.itemCategory.updateItemCategory.mutate(opts);
|
|
1291
|
-
out(result);
|
|
1292
|
-
}
|
|
1293
|
-
export async function deleteItemCategoriesByIds(ids) {
|
|
1294
|
-
const result = await api.itemCategory.deleteItemCategories.mutate({ ids });
|
|
1295
|
-
out(result);
|
|
1296
|
-
}
|
|
1297
|
-
export async function createBillFromPayload(raw) {
|
|
1298
|
-
const input = parseCreateBillPayload(raw);
|
|
1299
|
-
const result = await api.bill.create.mutate(input);
|
|
1300
|
-
out(result);
|
|
1301
|
-
}
|
|
1302
|
-
export async function listQuotations(opts) {
|
|
1303
|
-
const result = await api.quotation.list.query(opts);
|
|
1304
|
-
out(result);
|
|
1305
|
-
}
|
|
1306
|
-
export async function getQuotationDetails(id) {
|
|
1307
|
-
const result = await api.quotation.getById.query({ id });
|
|
1308
|
-
out(result);
|
|
1309
|
-
}
|
|
1310
|
-
export async function createQuotationFromPayload(raw) {
|
|
1311
|
-
const input = parseCreateQuotationPayload(raw);
|
|
1312
|
-
const result = await api.quotation.createDraft.mutate(input);
|
|
1313
|
-
out(result);
|
|
1314
|
-
}
|
|
1315
|
-
export const updateQuotationFromPayload = async (raw) => {
|
|
1316
|
-
const input = parseUpdateQuotationPayload(raw);
|
|
1317
|
-
const result = await api.quotation.updateDraftOrRejected.mutate(input);
|
|
1318
|
-
out(result);
|
|
1319
|
-
};
|
|
1320
|
-
export const deleteQuotationById = async (id) => {
|
|
1321
|
-
const result = await api.quotation.delete.mutate({ id });
|
|
1322
|
-
out(result);
|
|
1323
|
-
};
|
|
1324
|
-
export async function submitQuotation(id) {
|
|
1325
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1326
|
-
action: "Submit quotation for approval",
|
|
1327
|
-
entity: `quotation ${id}`,
|
|
1328
|
-
});
|
|
1329
|
-
const result = await api.quotation.submitForApproval.mutate({ id });
|
|
1330
|
-
out(result);
|
|
1331
|
-
}
|
|
1332
|
-
export async function downloadQuotationPdf(id, fileRole) {
|
|
1333
|
-
const result = await api.quotation.getSignedDownloadUrl.query({
|
|
1334
|
-
id,
|
|
1335
|
-
fileRole,
|
|
1336
|
-
});
|
|
1337
|
-
out(result);
|
|
1338
|
-
}
|
|
1339
|
-
export const checkCustomerInvoiceReadiness = async (budgetId) => {
|
|
1340
|
-
const result = await api.customerInvoice.getInvoiceReadiness.query({
|
|
1341
|
-
budgetId,
|
|
1342
|
-
});
|
|
1343
|
-
out(result);
|
|
1344
|
-
};
|
|
1345
|
-
export const listCustomerInvoices = async (input) => {
|
|
1346
|
-
const result = await api.customerInvoice.list.query(input);
|
|
1347
|
-
out(result);
|
|
1348
|
-
};
|
|
1349
|
-
export const listEligibleCustomerInvoiceBudgets = async (projectId) => {
|
|
1350
|
-
const result = await api.customerInvoice.getEligibleBudgets.query({
|
|
1351
|
-
projectId,
|
|
1352
|
-
});
|
|
1353
|
-
out(result);
|
|
1354
|
-
};
|
|
1355
|
-
export const getCustomerInvoice = async (batchId) => {
|
|
1356
|
-
const result = await api.customerInvoice.getInvoiceDetail.query({ batchId });
|
|
1357
|
-
out(result);
|
|
1358
|
-
};
|
|
1359
|
-
export const getCustomerInvoiceEmailContext = async (batchId) => {
|
|
1360
|
-
const result = await api.customerInvoice.getInvoiceEmailContext.query({
|
|
1361
|
-
batchId,
|
|
1362
|
-
});
|
|
1363
|
-
out(result);
|
|
1364
|
-
};
|
|
1365
|
-
export const createCustomerInvoice = async (raw) => {
|
|
1366
|
-
const input = parseCreateCustomerInvoicePayload(raw);
|
|
1367
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1368
|
-
action: "Create customer invoice",
|
|
1369
|
-
entity: `budget ${input.budgetId}`,
|
|
1370
|
-
details: "creates one invoice in QuickBooks; admin-created invoices are approved immediately, while other invoices request approval; approved cumulative coverage of 100% closes the QuickBooks estimate",
|
|
1371
|
-
});
|
|
1372
|
-
const result = await api.customerInvoice.createInvoiceBatch.mutate(input);
|
|
1373
|
-
out(result);
|
|
1374
|
-
};
|
|
1375
|
-
export const discardCreatingCustomerInvoice = async (batchId) => {
|
|
1376
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1377
|
-
action: "Discard unfinished customer invoice",
|
|
1378
|
-
entity: `invoice batch ${batchId}`,
|
|
1379
|
-
details: "removes only a reserved CREATING batch that has no invoice created in QuickBooks",
|
|
1380
|
-
});
|
|
1381
|
-
const result = await api.customerInvoice.discardCreatingInvoiceBatch.mutate({
|
|
1382
|
-
batchId,
|
|
1383
|
-
});
|
|
1384
|
-
out(result);
|
|
1385
|
-
};
|
|
1386
|
-
export const deleteCustomerInvoice = async (batchId) => {
|
|
1387
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1388
|
-
action: "Delete customer invoice",
|
|
1389
|
-
entity: `invoice batch ${batchId}`,
|
|
1390
|
-
details: "deletes its invoices from QuickBooks and removes the local batch; paid invoices are blocked until their QBO payments are reversed; removing approved coverage may reopen the estimate",
|
|
1391
|
-
});
|
|
1392
|
-
const result = await api.customerInvoice.deleteInvoiceBatch.mutate({
|
|
1393
|
-
batchId,
|
|
1394
|
-
});
|
|
1395
|
-
out(result);
|
|
1396
|
-
};
|
|
1397
|
-
export const voidCustomerInvoice = async (batchId) => {
|
|
1398
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1399
|
-
action: "Void customer invoice",
|
|
1400
|
-
entity: `invoice batch ${batchId}`,
|
|
1401
|
-
details: "voids its invoices in QuickBooks and updates Budget Builder; paid invoices are blocked until their QBO payments are reversed; removing approved coverage may reopen the estimate",
|
|
1402
|
-
});
|
|
1403
|
-
const result = await api.customerInvoice.voidInvoiceBatch.mutate({ batchId });
|
|
1404
|
-
out(result);
|
|
1405
|
-
};
|
|
1406
|
-
export const approveCustomerInvoice = async (batchId) => {
|
|
1407
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1408
|
-
action: "Approve customer invoice",
|
|
1409
|
-
entity: `invoice batch ${batchId}`,
|
|
1410
|
-
details: "approves the batch and notifies its creator; voided invoices cannot be approved; cumulative approved coverage of 100% closes the QuickBooks estimate",
|
|
1411
|
-
});
|
|
1412
|
-
const result = await api.customerInvoice.approveInvoiceBatch.mutate({
|
|
1413
|
-
batchId,
|
|
1414
|
-
});
|
|
1415
|
-
out(result);
|
|
1416
|
-
};
|
|
1417
|
-
export const rejectCustomerInvoice = async (batchId, rejectionReason) => {
|
|
1418
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1419
|
-
action: "Reject customer invoice",
|
|
1420
|
-
entity: `invoice batch ${batchId}`,
|
|
1421
|
-
details: `records the rejection reason, voids it in QuickBooks, notifies its creator, and reopens a prematurely closed estimate when applicable`,
|
|
1422
|
-
});
|
|
1423
|
-
const result = await api.customerInvoice.rejectInvoiceBatch.mutate({
|
|
1424
|
-
batchId,
|
|
1425
|
-
rejectionReason,
|
|
1426
|
-
});
|
|
1427
|
-
out(result);
|
|
1428
|
-
};
|
|
1429
|
-
export const downloadCustomerInvoicePdf = async (invoiceId, outputPath) => {
|
|
1430
|
-
const result = await api.customerInvoice.downloadPdf.mutate({ invoiceId });
|
|
1431
|
-
const resolvedOutputPath = outputPath?.trim() || result.fileName;
|
|
1432
|
-
const contents = Buffer.from(result.base64, "base64");
|
|
1433
|
-
await writeFile(resolvedOutputPath, contents);
|
|
1434
|
-
out({
|
|
1435
|
-
contentType: result.contentType,
|
|
1436
|
-
fileName: result.fileName,
|
|
1437
|
-
outputPath: resolvedOutputPath,
|
|
1438
|
-
size: contents.byteLength,
|
|
1439
|
-
});
|
|
1440
|
-
};
|
|
1441
|
-
export const syncCustomerInvoice = async (invoiceId) => {
|
|
1442
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1443
|
-
action: "Sync customer invoice from QuickBooks",
|
|
1444
|
-
entity: `invoice ${invoiceId}`,
|
|
1445
|
-
details: "refreshes local status, QuickBooks metadata, balance, and history; QBO Paid or a zero balance becomes PAID, a reversed payment restores the delivery or approval status, and a QBO void becomes VOIDED",
|
|
1446
|
-
});
|
|
1447
|
-
const result = await api.customerInvoice.syncInvoiceStatus.mutate({
|
|
1448
|
-
invoiceId,
|
|
1449
|
-
});
|
|
1450
|
-
out(result);
|
|
1451
|
-
};
|
|
1452
|
-
export const describeCustomerInvoiceEmailConfirmation = (input) => `emails the QBO invoice PDF to ${input.to} and saves that address as the project's billing email; requested CC ${input.cc.length > 0 ? input.cc.join(", ") : "none"}; reply-to ${input.replyTo}; subject "${input.subject}"; server-required admin, creator, business-development, and inside-sales CC recipients are added; success marks the invoice SENT; do not retry blindly after an ambiguous delivery failure`;
|
|
1453
|
-
export const sendCustomerInvoiceToContactPersonFromPayload = async (raw) => {
|
|
1454
|
-
const input = parseSendCustomerInvoiceToContactPersonPayload(raw);
|
|
1455
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1456
|
-
action: "Send customer invoice email",
|
|
1457
|
-
entity: `invoice ${input.invoiceId}`,
|
|
1458
|
-
details: describeCustomerInvoiceEmailConfirmation(input),
|
|
1459
|
-
});
|
|
1460
|
-
const result = await api.customerInvoice.sendInvoiceToContactPerson.mutate(input);
|
|
1461
|
-
out(result);
|
|
1462
|
-
};
|
|
1463
|
-
export async function updateBillFromPayload(raw) {
|
|
1464
|
-
const input = parseUpdateBillPayload(raw);
|
|
1465
|
-
const result = await api.bill.update.mutate(input);
|
|
1466
|
-
out(result);
|
|
1467
|
-
}
|
|
1468
|
-
export const validateBillSelectionFromPayload = async (raw) => {
|
|
1469
|
-
const input = parseValidateBillSelectionPayload(raw);
|
|
1470
|
-
const result = await api.bill.validateSelection.mutate(input);
|
|
1471
|
-
out(result);
|
|
1472
|
-
};
|
|
1473
|
-
export const updateBillPaymentEvidenceFromPayload = async (raw) => {
|
|
1474
|
-
const input = parseUpdateBillPaymentEvidencePayload(raw);
|
|
1475
|
-
const result = await api.bill.updatePaymentEvidence.mutate(input);
|
|
1476
|
-
out(result);
|
|
1477
|
-
};
|
|
1478
|
-
export async function deleteBillById(id) {
|
|
1479
|
-
const result = await api.bill.delete.mutate({ id });
|
|
1480
|
-
out(result);
|
|
1481
|
-
}
|
|
1482
|
-
export async function createSupplierFromPayload(raw) {
|
|
1483
|
-
const input = parseSupplierCreatePayload(raw);
|
|
1484
|
-
const created = await api.supplier.createSupplier.mutate(input);
|
|
1485
|
-
const supplier = created.result;
|
|
1486
|
-
if (supplier.status !== "PENDING_APPROVAL") {
|
|
1487
|
-
out(created);
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1491
|
-
action: "Create supplier approval request",
|
|
1492
|
-
entity: `supplier ${supplier.id}`,
|
|
1493
|
-
details: "and send approval request emails",
|
|
1494
|
-
});
|
|
1495
|
-
const approval = await api.supplier.createSupplierApproval.mutate({
|
|
1496
|
-
supplierId: supplier.id,
|
|
1497
|
-
});
|
|
1498
|
-
let emailResult;
|
|
1499
|
-
const approvalIds = (approval.results ?? []).map((item) => ({ id: item.id }));
|
|
1500
|
-
if (approvalIds.length === 0) {
|
|
1501
|
-
emailResult = createApprovalEmailDeliveryFailure("No approval ids returned");
|
|
1502
|
-
}
|
|
1503
|
-
else {
|
|
1504
|
-
try {
|
|
1505
|
-
const delivery = approval.emailDelivery;
|
|
1506
|
-
emailResult = createApprovalEmailDeliveryReport(delivery);
|
|
1507
|
-
}
|
|
1508
|
-
catch (error) {
|
|
1509
|
-
emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
out({
|
|
1513
|
-
...created,
|
|
1514
|
-
supplierApproval: {
|
|
1515
|
-
requested: true,
|
|
1516
|
-
approvalCount: approvalIds.length,
|
|
1517
|
-
email: emailResult,
|
|
1518
|
-
},
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
export async function updateSupplierFromPayload(raw) {
|
|
1522
|
-
const input = parseSupplierUpdatePayload(raw);
|
|
1523
|
-
const updated = await api.supplier.updateSupplier.mutate(input);
|
|
1524
|
-
const supplier = updated.result;
|
|
1525
|
-
if (supplier.status !== "PENDING_APPROVAL") {
|
|
1526
|
-
out(updated);
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1530
|
-
action: "Create supplier approval request",
|
|
1531
|
-
entity: `supplier ${supplier.id}`,
|
|
1532
|
-
details: "and send approval request emails",
|
|
1533
|
-
});
|
|
1534
|
-
let approvalResult;
|
|
1535
|
-
try {
|
|
1536
|
-
const approval = await api.supplier.createSupplierApproval.mutate({
|
|
1537
|
-
supplierId: supplier.id,
|
|
1538
|
-
});
|
|
1539
|
-
const approvalIds = (approval.results ?? []).map((item) => ({
|
|
1540
|
-
id: item.id,
|
|
1541
|
-
}));
|
|
1542
|
-
if (approvalIds.length === 0) {
|
|
1543
|
-
approvalResult = {
|
|
1544
|
-
requested: true,
|
|
1545
|
-
approvalCount: 0,
|
|
1546
|
-
email: createApprovalEmailDeliveryFailure("No approval ids returned"),
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
else {
|
|
1550
|
-
try {
|
|
1551
|
-
const delivery = approval.emailDelivery;
|
|
1552
|
-
approvalResult = {
|
|
1553
|
-
requested: true,
|
|
1554
|
-
approvalCount: approvalIds.length,
|
|
1555
|
-
email: createApprovalEmailDeliveryReport(delivery),
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
|
-
catch (error) {
|
|
1559
|
-
approvalResult = {
|
|
1560
|
-
requested: true,
|
|
1561
|
-
approvalCount: approvalIds.length,
|
|
1562
|
-
email: createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error)),
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
catch (error) {
|
|
1568
|
-
approvalResult = {
|
|
1569
|
-
requested: false,
|
|
1570
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
out({
|
|
1574
|
-
...updated,
|
|
1575
|
-
supplierApproval: approvalResult,
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
export async function createItemFromPayload(raw) {
|
|
1579
|
-
const input = parseItemCreatePayload(raw);
|
|
1580
|
-
const result = await api.item.createItem.mutate(input);
|
|
1581
|
-
out(result);
|
|
1582
|
-
}
|
|
1583
|
-
export async function updateItemFromPayload(raw) {
|
|
1584
|
-
const input = parseItemUpdatePayload(raw);
|
|
1585
|
-
const result = await api.item.updateItem.mutate(input);
|
|
1586
|
-
out(result);
|
|
1587
|
-
}
|
|
1588
|
-
export async function createContactPersonFromPayload(raw) {
|
|
1589
|
-
const input = parseContactCreatePayload(raw);
|
|
1590
|
-
const result = await api.contactPerson.createContactPerson.mutate(input);
|
|
1591
|
-
out(result);
|
|
1592
|
-
}
|
|
1593
|
-
export async function updateContactPersonFromPayload(raw) {
|
|
1594
|
-
const input = parseContactUpdatePayload(raw);
|
|
1595
|
-
const result = await api.contactPerson.updateContactPerson.mutate(input);
|
|
1596
|
-
out(result);
|
|
1597
|
-
}
|
|
1598
|
-
export async function updateProjectFromPayload(raw) {
|
|
1599
|
-
const input = parseUpdateProjectPayload(raw);
|
|
1600
|
-
const result = await api.project.updateProject.mutate(input);
|
|
1601
|
-
out(result);
|
|
1602
|
-
}
|
|
1603
|
-
export async function updateCompanyFromPayload(raw) {
|
|
1604
|
-
const input = parseCompanyUpdatePayload(raw);
|
|
1605
|
-
const result = await api.company.updateCompany.mutate(input);
|
|
1606
|
-
out(result);
|
|
1607
|
-
}
|
|
1608
|
-
// --- Approvals: budgets / suppliers / bills (MCP parity) ---
|
|
1609
|
-
export async function approveBudget(budgetId) {
|
|
1610
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1611
|
-
action: "Approve budget",
|
|
1612
|
-
entity: `budget ${budgetId}`,
|
|
1613
|
-
details: "and send requester reply email",
|
|
1614
|
-
});
|
|
1615
|
-
const notifications = await getAllNotifications();
|
|
1616
|
-
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1617
|
-
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1618
|
-
n.type === ExtendedApprovalType.BUDGET &&
|
|
1619
|
-
(n.budgetId === budgetId ||
|
|
1620
|
-
n.budget?.id === budgetId));
|
|
1621
|
-
const approval = pending[0];
|
|
1622
|
-
if (!approval) {
|
|
1623
|
-
throw new Error("No pending budget approval found for this budget and approver.");
|
|
1624
|
-
}
|
|
1625
|
-
const result = await api.budget.updateBudgetApproval.mutate({
|
|
1626
|
-
budgetApprovalId: approval.id,
|
|
1627
|
-
status: ExtendedApprovalStatus.APPROVED,
|
|
1628
|
-
});
|
|
1629
|
-
let email;
|
|
1630
|
-
try {
|
|
1631
|
-
const delivery = result.emailDelivery;
|
|
1632
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
1633
|
-
}
|
|
1634
|
-
catch (error) {
|
|
1635
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1636
|
-
}
|
|
1637
|
-
out({
|
|
1638
|
-
...result,
|
|
1639
|
-
email,
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
export async function rejectBudget(budgetId, reason) {
|
|
1643
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1644
|
-
action: "Reject budget",
|
|
1645
|
-
entity: `budget ${budgetId}`,
|
|
1646
|
-
details: `with reason "${reason}" and send requester reply email`,
|
|
1647
|
-
});
|
|
1648
|
-
const notifications = await getAllNotifications();
|
|
1649
|
-
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1650
|
-
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1651
|
-
n.type === ExtendedApprovalType.BUDGET &&
|
|
1652
|
-
(n.budgetId === budgetId ||
|
|
1653
|
-
n.budget?.id === budgetId));
|
|
1654
|
-
const approval = pending[0];
|
|
1655
|
-
if (!approval) {
|
|
1656
|
-
throw new Error("No pending budget approval found for this budget and approver.");
|
|
1657
|
-
}
|
|
1658
|
-
const result = await api.budget.updateBudgetApproval.mutate({
|
|
1659
|
-
budgetApprovalId: approval.id,
|
|
1660
|
-
status: ExtendedApprovalStatus.REJECTED,
|
|
1661
|
-
rejectionReason: reason,
|
|
1662
|
-
});
|
|
1663
|
-
let email;
|
|
1664
|
-
try {
|
|
1665
|
-
const delivery = result.emailDelivery;
|
|
1666
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
1667
|
-
}
|
|
1668
|
-
catch (error) {
|
|
1669
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1670
|
-
}
|
|
1671
|
-
out({
|
|
1672
|
-
...result,
|
|
1673
|
-
email,
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
export async function getSupplierDetails(supplierId) {
|
|
1677
|
-
const supplier = await api.supplier.getSupplierById.query({
|
|
1678
|
-
id: supplierId,
|
|
1679
|
-
});
|
|
1680
|
-
const respondedStatuses = new Set([
|
|
1681
|
-
ExtendedApprovalStatus.APPROVED,
|
|
1682
|
-
ExtendedApprovalStatus.REJECTED,
|
|
1683
|
-
]);
|
|
1684
|
-
const latestRespondedApproval = [
|
|
1685
|
-
...supplier.approvals.filter((approval) => respondedStatuses.has(approval.status)),
|
|
1686
|
-
].sort((a, b) => new Date(b.respondedAt ?? b.updatedAt).getTime() -
|
|
1687
|
-
new Date(a.respondedAt ?? a.updatedAt).getTime() ||
|
|
1688
|
-
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
1689
|
-
const pendingApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.PENDING_APPROVAL);
|
|
1690
|
-
const supersededApprovals = supplier.approvals.filter((approval) => approval.status === ExtendedApprovalStatus.SUPERSEDED);
|
|
1691
|
-
const respondedByApproval = latestRespondedApproval ?? null;
|
|
1692
|
-
const responseUser = respondedByApproval
|
|
1693
|
-
? (respondedByApproval.responder ?? respondedByApproval.approver)
|
|
1694
|
-
: null;
|
|
1695
|
-
out({
|
|
1696
|
-
...supplier,
|
|
1697
|
-
supplierApprovalSummary: {
|
|
1698
|
-
pendingApprovalCount: pendingApprovals.length,
|
|
1699
|
-
pendingApprovers: pendingApprovals.map((approval) => ({
|
|
1700
|
-
approvalId: approval.id,
|
|
1701
|
-
approverId: approval.approverId,
|
|
1702
|
-
name: approval.approver.name,
|
|
1703
|
-
email: approval.approver.email,
|
|
1704
|
-
createdAt: approval.createdAt,
|
|
1705
|
-
updatedAt: approval.updatedAt,
|
|
1706
|
-
})),
|
|
1707
|
-
supersededApprovalCount: supersededApprovals.length,
|
|
1708
|
-
supersededApprovers: supersededApprovals.map((approval) => ({
|
|
1709
|
-
approvalId: approval.id,
|
|
1710
|
-
approverId: approval.approverId,
|
|
1711
|
-
name: approval.approver.name,
|
|
1712
|
-
email: approval.approver.email,
|
|
1713
|
-
respondedAt: approval.respondedAt,
|
|
1714
|
-
responder: approval.responder
|
|
1715
|
-
? {
|
|
1716
|
-
id: approval.responder.id,
|
|
1717
|
-
name: approval.responder.name,
|
|
1718
|
-
email: approval.responder.email,
|
|
1719
|
-
}
|
|
1720
|
-
: null,
|
|
1721
|
-
})),
|
|
1722
|
-
respondedBy: respondedByApproval && responseUser
|
|
1723
|
-
? {
|
|
1724
|
-
approvalId: respondedByApproval.id,
|
|
1725
|
-
status: respondedByApproval.status,
|
|
1726
|
-
approverId: respondedByApproval.approverId,
|
|
1727
|
-
responderId: respondedByApproval.responderId,
|
|
1728
|
-
name: responseUser.name,
|
|
1729
|
-
email: responseUser.email,
|
|
1730
|
-
respondedAt: respondedByApproval.respondedAt,
|
|
1731
|
-
updatedAt: respondedByApproval.updatedAt,
|
|
1732
|
-
}
|
|
1733
|
-
: null,
|
|
1734
|
-
},
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
export async function approveSupplier(supplierId) {
|
|
1738
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1739
|
-
action: "Approve supplier",
|
|
1740
|
-
entity: `supplier ${supplierId}`,
|
|
1741
|
-
details: "and send requester reply email",
|
|
1742
|
-
});
|
|
1743
|
-
const notifications = await getAllNotifications();
|
|
1744
|
-
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1745
|
-
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1746
|
-
n.type === ExtendedApprovalType.SUPPLIER &&
|
|
1747
|
-
(n.supplierId === supplierId || n.supplier?.id === supplierId));
|
|
1748
|
-
const approval = pending[0];
|
|
1749
|
-
if (!approval) {
|
|
1750
|
-
throw new Error("No pending supplier approval found.");
|
|
1751
|
-
}
|
|
1752
|
-
const result = await api.supplier.updateSupplierApproval.mutate({
|
|
1753
|
-
supplierApprovalId: approval.id,
|
|
1754
|
-
status: ExtendedApprovalStatus.APPROVED,
|
|
1755
|
-
});
|
|
1756
|
-
let email;
|
|
1757
|
-
try {
|
|
1758
|
-
const delivery = result.emailDelivery;
|
|
1759
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
1760
|
-
}
|
|
1761
|
-
catch (error) {
|
|
1762
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1763
|
-
}
|
|
1764
|
-
out({
|
|
1765
|
-
...result,
|
|
1766
|
-
email,
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
export async function rejectSupplier(supplierId, reason) {
|
|
1770
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1771
|
-
action: "Reject supplier",
|
|
1772
|
-
entity: `supplier ${supplierId}`,
|
|
1773
|
-
details: `with reason "${reason}" and send requester reply email`,
|
|
1774
|
-
});
|
|
1775
|
-
const notifications = await getAllNotifications();
|
|
1776
|
-
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1777
|
-
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1778
|
-
n.type === ExtendedApprovalType.SUPPLIER &&
|
|
1779
|
-
(n.supplierId === supplierId || n.supplier?.id === supplierId));
|
|
1780
|
-
const approval = pending[0];
|
|
1781
|
-
if (!approval) {
|
|
1782
|
-
throw new Error("No pending supplier approval found.");
|
|
1783
|
-
}
|
|
1784
|
-
const result = await api.supplier.updateSupplierApproval.mutate({
|
|
1785
|
-
supplierApprovalId: approval.id,
|
|
1786
|
-
status: ExtendedApprovalStatus.REJECTED,
|
|
1787
|
-
rejectionReason: reason,
|
|
1788
|
-
});
|
|
1789
|
-
let email;
|
|
1790
|
-
try {
|
|
1791
|
-
const delivery = result.emailDelivery;
|
|
1792
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
1793
|
-
}
|
|
1794
|
-
catch (error) {
|
|
1795
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1796
|
-
}
|
|
1797
|
-
out({
|
|
1798
|
-
...result,
|
|
1799
|
-
email,
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
export async function rejectBill(billId, reason) {
|
|
1803
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1804
|
-
action: "Reject bill",
|
|
1805
|
-
entity: `bill ${billId}`,
|
|
1806
|
-
details: `with reason "${reason}" and send requester reply email`,
|
|
1807
|
-
});
|
|
1808
|
-
const notifications = await getAllNotifications();
|
|
1809
|
-
const pending = notifications.filter((n) => n.notificationType === "request" &&
|
|
1810
|
-
n.status === ExtendedApprovalStatus.PENDING_APPROVAL &&
|
|
1811
|
-
n.type === ExtendedApprovalType.BILL &&
|
|
1812
|
-
(n.billId === billId || n.bill?.id === billId));
|
|
1813
|
-
const approval = pending[0];
|
|
1814
|
-
if (!approval) {
|
|
1815
|
-
throw new Error("No pending bill approval found.");
|
|
1816
|
-
}
|
|
1817
|
-
const result = await api.bill.updateApproval.mutate({
|
|
1818
|
-
billApprovalId: approval.id,
|
|
1819
|
-
status: ExtendedApprovalStatus.REJECTED,
|
|
1820
|
-
rejectionReason: reason,
|
|
1821
|
-
});
|
|
1822
|
-
if (result.processing) {
|
|
1823
|
-
out(result);
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
let email;
|
|
1827
|
-
try {
|
|
1828
|
-
const delivery = result.emailDelivery;
|
|
1829
|
-
email = createApprovalEmailDeliveryReport(delivery);
|
|
1830
|
-
}
|
|
1831
|
-
catch (error) {
|
|
1832
|
-
email = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
1833
|
-
}
|
|
1834
|
-
out({
|
|
1835
|
-
...result,
|
|
1836
|
-
email,
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
export async function approveQuotation(quotationId) {
|
|
1840
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1841
|
-
action: "Approve quotation",
|
|
1842
|
-
entity: `quotation ${quotationId}`,
|
|
1843
|
-
});
|
|
1844
|
-
const notifications = await getAllNotifications();
|
|
1845
|
-
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1846
|
-
n.type === QUOTATION &&
|
|
1847
|
-
(n.supplierQuotationId === quotationId ||
|
|
1848
|
-
n.supplierQuotation?.id === quotationId));
|
|
1849
|
-
const approval = pending[0];
|
|
1850
|
-
if (!approval) {
|
|
1851
|
-
throw new Error("No pending quotation approval found.");
|
|
1852
|
-
}
|
|
1853
|
-
const result = await api.quotation.approve.mutate({
|
|
1854
|
-
quotationApprovalId: approval.id,
|
|
1855
|
-
});
|
|
1856
|
-
out(result);
|
|
1857
|
-
}
|
|
1858
|
-
export async function rejectQuotation(quotationId, reason) {
|
|
1859
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1860
|
-
action: "Reject quotation",
|
|
1861
|
-
entity: `quotation ${quotationId}`,
|
|
1862
|
-
details: `with reason "${reason}"`,
|
|
1863
|
-
});
|
|
1864
|
-
const notifications = await getAllNotifications();
|
|
1865
|
-
const pending = notifications.filter((n) => isPendingRequest(n) &&
|
|
1866
|
-
n.type === QUOTATION &&
|
|
1867
|
-
(n.supplierQuotationId === quotationId ||
|
|
1868
|
-
n.supplierQuotation?.id === quotationId));
|
|
1869
|
-
const approval = pending[0];
|
|
1870
|
-
if (!approval) {
|
|
1871
|
-
throw new Error("No pending quotation approval found.");
|
|
1872
|
-
}
|
|
1873
|
-
const result = await api.quotation.reject.mutate({
|
|
1874
|
-
quotationApprovalId: approval.id,
|
|
1875
|
-
rejectionReason: reason,
|
|
1876
|
-
});
|
|
1877
|
-
out(result);
|
|
1878
|
-
}
|
|
1879
|
-
// --- Companies & projects ---
|
|
1880
|
-
export async function listCompanies(opts) {
|
|
1881
|
-
const result = await api.company.getCompanies.query({
|
|
1882
|
-
name: opts.name,
|
|
1883
|
-
createdBy: opts.createdBy,
|
|
1884
|
-
active: opts.active,
|
|
1885
|
-
page: opts.page ?? 1,
|
|
1886
|
-
perPage: opts.perPage ?? 20,
|
|
1887
|
-
});
|
|
1888
|
-
out(result);
|
|
1889
|
-
}
|
|
1890
|
-
export async function getCompany(id) {
|
|
1891
|
-
const company = await api.company.getCompanyById.query({ id });
|
|
1892
|
-
out(company);
|
|
1893
|
-
}
|
|
1894
|
-
export async function createCompany(opts) {
|
|
1895
|
-
const result = await api.company.createCompany.mutate(opts);
|
|
1896
|
-
out(result);
|
|
1897
|
-
}
|
|
1898
|
-
export async function listProjects(opts) {
|
|
1899
|
-
const result = await api.project.getAllProjects.query({
|
|
1900
|
-
companyId: opts.companyId,
|
|
1901
|
-
name: opts.name,
|
|
1902
|
-
status: opts.status,
|
|
1903
|
-
active: opts.active,
|
|
1904
|
-
page: opts.page ?? 1,
|
|
1905
|
-
perPage: opts.perPage ?? 20,
|
|
1906
|
-
});
|
|
1907
|
-
out(result);
|
|
1908
|
-
}
|
|
1909
|
-
export const listProjectHubs = async (opts) => {
|
|
1910
|
-
const result = await api.project.getAllProjectHubs.query({
|
|
1911
|
-
...opts,
|
|
1912
|
-
page: opts.page ?? 1,
|
|
1913
|
-
perPage: opts.perPage ?? 20,
|
|
1914
|
-
});
|
|
1915
|
-
out(result);
|
|
1916
|
-
};
|
|
1917
|
-
export async function getProject(id) {
|
|
1918
|
-
const [project, budgetsOverview, billsOverview] = await Promise.all([
|
|
1919
|
-
api.project.getProjectDetailsById.query({ id }),
|
|
1920
|
-
api.project.getProjectBudgetsOverview.query({ projectId: id }),
|
|
1921
|
-
api.project.getProjectBillsOverview.query({ projectId: id }),
|
|
1922
|
-
]);
|
|
1923
|
-
out({ project, budgetsOverview, billsOverview });
|
|
1924
|
-
}
|
|
1925
|
-
export const getProjectHubStatus = async (projectId) => {
|
|
1926
|
-
const result = await api.project.getProjectHubShareLink.query({ projectId });
|
|
1927
|
-
out(result);
|
|
1928
|
-
};
|
|
1929
|
-
export const setupProjectHub = async (projectId) => {
|
|
1930
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1931
|
-
action: "Set up Project Hub",
|
|
1932
|
-
entity: `project ${projectId}`,
|
|
1933
|
-
details: "queue Asana, Drive, and client Hub provisioning",
|
|
1934
|
-
});
|
|
1935
|
-
const result = await api.project.setupProjectHub.mutate({ projectId });
|
|
1936
|
-
out(result);
|
|
1937
|
-
};
|
|
1938
|
-
export const syncProjectHubCommercialDocuments = async (projectId) => {
|
|
1939
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1940
|
-
action: "Sync Project Hub commercial documents",
|
|
1941
|
-
entity: `project ${projectId}`,
|
|
1942
|
-
details: "queue a full Budget Builder to Google Drive and Asana reconciliation",
|
|
1943
|
-
});
|
|
1944
|
-
const result = await api.project.syncProjectHubCommercialDocuments.mutate({
|
|
1945
|
-
projectId,
|
|
1946
|
-
});
|
|
1947
|
-
out(result);
|
|
1948
|
-
};
|
|
1949
|
-
export async function createProject(opts) {
|
|
1950
|
-
const links = await resolveCreateProjectLinks({
|
|
1951
|
-
name: opts.name,
|
|
1952
|
-
asanaTaskId: opts.asanaTaskId,
|
|
1953
|
-
asanaSearch: opts.asanaSearch,
|
|
1954
|
-
slackChannelId: opts.slackChannelId,
|
|
1955
|
-
slackChannelUrl: opts.slackChannelUrl,
|
|
1956
|
-
slackChannelName: opts.slackChannelName,
|
|
1957
|
-
});
|
|
1958
|
-
const result = await api.project.createProject.mutate({
|
|
1959
|
-
name: opts.name,
|
|
1960
|
-
description: opts.description,
|
|
1961
|
-
companyId: opts.companyId,
|
|
1962
|
-
contactPersonId: opts.contactPersonId,
|
|
1963
|
-
insideSalesId: opts.insideSalesId,
|
|
1964
|
-
businessDevelopmentId: opts.businessDevelopmentId,
|
|
1965
|
-
projectManagerId: opts.projectManagerId,
|
|
1966
|
-
venue: opts.venue,
|
|
1967
|
-
pax: opts.pax,
|
|
1968
|
-
asanaTaskId: links.asanaTaskId,
|
|
1969
|
-
slackChannelId: links.slackChannelId,
|
|
1970
|
-
slackChannelUrl: links.slackChannelUrl,
|
|
1971
|
-
slackChannelName: links.slackChannelName,
|
|
1972
|
-
dateRange: {
|
|
1973
|
-
from: new Date(opts.startDate),
|
|
1974
|
-
to: opts.endDate ? new Date(opts.endDate) : undefined,
|
|
1975
|
-
},
|
|
1976
|
-
});
|
|
1977
|
-
out(result);
|
|
1978
|
-
}
|
|
1979
|
-
export async function updateProjectStatus(id, status, opts) {
|
|
1980
|
-
await assertSensitiveWorkflowConfirmed({
|
|
1981
|
-
action: "Update project status",
|
|
1982
|
-
entity: `project ${id}`,
|
|
1983
|
-
details: `to ${status}`,
|
|
1984
|
-
});
|
|
1985
|
-
const result = await api.project.updateProjectStatus.mutate({
|
|
1986
|
-
id,
|
|
1987
|
-
status,
|
|
1988
|
-
...(opts?.projectManagerId && { projectManagerId: opts.projectManagerId }),
|
|
1989
|
-
...(opts?.projectManagerEmail && {
|
|
1990
|
-
projectManagerEmail: opts.projectManagerEmail,
|
|
1991
|
-
}),
|
|
1992
|
-
...(opts?.wonOverrideReason && {
|
|
1993
|
-
wonOverrideReason: opts.wonOverrideReason,
|
|
1994
|
-
}),
|
|
1995
|
-
});
|
|
1996
|
-
out(result);
|
|
1997
|
-
}
|
|
1998
|
-
export const checkProjectReconciliation = async (projectId) => {
|
|
1999
|
-
const result = await api.project.getReconciliationReview.query({ projectId });
|
|
2000
|
-
out(result);
|
|
2001
|
-
};
|
|
2002
|
-
export const reconcileProject = async (projectId) => {
|
|
2003
|
-
await assertSensitiveWorkflowConfirmed({
|
|
2004
|
-
action: "Reconcile project",
|
|
2005
|
-
entity: `project ${projectId}`,
|
|
2006
|
-
details: "rerun all reconciliation checks and mark it RECONCILED",
|
|
2007
|
-
});
|
|
2008
|
-
const result = await api.project.reconcile.mutate({ projectId });
|
|
2009
|
-
out(result);
|
|
2010
|
-
};
|
|
2011
|
-
export const completeProject = async (projectId) => {
|
|
2012
|
-
await updateProjectStatus(projectId, ProjectStatus.COMPLETED);
|
|
2013
|
-
};
|
|
2014
|
-
export const importQuickBooksProjectId = async (projectId, projectUrl) => {
|
|
2015
|
-
const result = await api.project.importQboProjectId.mutate({
|
|
2016
|
-
projectId,
|
|
2017
|
-
projectUrl,
|
|
2018
|
-
});
|
|
2019
|
-
out(result);
|
|
2020
|
-
};
|
|
2021
|
-
export const listIntegrationOperations = async (input) => {
|
|
2022
|
-
const result = await api.integration.listOperations.query(input);
|
|
2023
|
-
out(result);
|
|
2024
|
-
};
|
|
2025
|
-
export const retryIntegrationOperation = async (operationId, confirmExternalStateReconciled, outboundEmailResolution, providerMessageId) => {
|
|
2026
|
-
await assertSensitiveWorkflowConfirmed({
|
|
2027
|
-
action: outboundEmailResolution === "ACCEPTED"
|
|
2028
|
-
? "Reconcile integration operation as completed"
|
|
2029
|
-
: "Retry integration operation",
|
|
2030
|
-
entity: `operation ${operationId}`,
|
|
2031
|
-
details: outboundEmailResolution === "ACCEPTED"
|
|
2032
|
-
? `mark provider-accepted email sent with message ID ${providerMessageId ?? "<missing>"}`
|
|
2033
|
-
: outboundEmailResolution === "NOT_ACCEPTED_RETRY"
|
|
2034
|
-
? "rotate the Resend idempotency key and retry provider delivery"
|
|
2035
|
-
: confirmExternalStateReconciled
|
|
2036
|
-
? "after confirming the external state has been reconciled"
|
|
2037
|
-
: undefined,
|
|
2038
|
-
});
|
|
2039
|
-
const result = await api.integration.retryOperation.mutate({
|
|
2040
|
-
operationId,
|
|
2041
|
-
confirmExternalStateReconciled,
|
|
2042
|
-
outboundEmailResolution,
|
|
2043
|
-
providerMessageId,
|
|
2044
|
-
});
|
|
2045
|
-
out(result);
|
|
2046
|
-
};
|
|
2047
|
-
// --- Typeform inbound administration ---
|
|
2048
|
-
export const listInboundSources = async (input) => {
|
|
2049
|
-
const result = await api.inbound.listSources.query(input);
|
|
2050
|
-
out(result);
|
|
2051
|
-
};
|
|
2052
|
-
export const createInboundSource = async (input) => {
|
|
2053
|
-
const result = await api.inbound.createSource.mutate(input);
|
|
2054
|
-
out(result);
|
|
2055
|
-
};
|
|
2056
|
-
export const getInboundSource = async (sourceId) => {
|
|
2057
|
-
const result = await api.inbound.getSource.query({ sourceId });
|
|
2058
|
-
out(result);
|
|
2059
|
-
};
|
|
2060
|
-
export const updateInboundSource = async (sourceId, name) => {
|
|
2061
|
-
const result = await api.inbound.updateSource.mutate({ name, sourceId });
|
|
2062
|
-
out(result);
|
|
2063
|
-
};
|
|
2064
|
-
export const listInboundSubmissions = async (input) => {
|
|
2065
|
-
const result = await api.inbound.listSubmissions.query(input);
|
|
2066
|
-
out(result);
|
|
2067
|
-
};
|
|
2068
|
-
export const getInboundSubmission = async (submissionId) => {
|
|
2069
|
-
const result = await api.inbound.getSubmission.query({ submissionId });
|
|
2070
|
-
out(result);
|
|
2071
|
-
};
|
|
2072
|
-
export const saveInboundMappingVersion = async (sourceId, mappings) => {
|
|
2073
|
-
const source = await api.inbound.getSource.query({ sourceId });
|
|
2074
|
-
const result = await api.inbound.saveMappingVersion.mutate({
|
|
2075
|
-
expectedActiveMappingVersion: source.activeMappingVersion,
|
|
2076
|
-
mappings,
|
|
2077
|
-
sourceId,
|
|
2078
|
-
});
|
|
2079
|
-
out(result);
|
|
2080
|
-
};
|
|
2081
|
-
export const saveInboundAutomationConfig = async (input) => {
|
|
2082
|
-
const result = await api.inbound.saveAutomationConfig.mutate(input);
|
|
2083
|
-
out(result);
|
|
2084
|
-
};
|
|
2085
|
-
export const updateInboundProcessing = async (input) => {
|
|
2086
|
-
const result = await api.inbound.updateProcessing.mutate(input);
|
|
2087
|
-
out(result);
|
|
2088
|
-
};
|
|
2089
|
-
// --- Contacts ---
|
|
2090
|
-
export async function listContacts(companyId) {
|
|
2091
|
-
const contacts = await api.contactPerson.getContactPersonByCompanyId.query({
|
|
2092
|
-
companyId,
|
|
2093
|
-
});
|
|
2094
|
-
out(contacts);
|
|
2095
|
-
}
|
|
2096
|
-
// --- Items ---
|
|
2097
|
-
export async function listItems(opts) {
|
|
2098
|
-
const result = await api.item.getItems.query({
|
|
2099
|
-
includeQuickBooksDeactivationPending: opts.includeQuickBooksDeactivationPending,
|
|
2100
|
-
page: opts.page ?? 1,
|
|
2101
|
-
perPage: opts.perPage ?? 20,
|
|
2102
|
-
name: opts.name,
|
|
2103
|
-
});
|
|
2104
|
-
out(result);
|
|
2105
|
-
}
|
|
2106
|
-
export async function getItem(id) {
|
|
2107
|
-
const item = await api.item.getItem.query({ id });
|
|
2108
|
-
out(item);
|
|
2109
|
-
}
|
|
2110
|
-
export async function listItemCategories(opts) {
|
|
2111
|
-
const result = await api.itemCategory.getItemCategories.query({
|
|
2112
|
-
page: opts.page ?? 1,
|
|
2113
|
-
perPage: opts.perPage ?? 50,
|
|
2114
|
-
});
|
|
2115
|
-
out(result);
|
|
2116
|
-
}
|
|
2117
|
-
// --- Supplier analytics ---
|
|
2118
|
-
export async function getSupplierAnalytics(opts) {
|
|
2119
|
-
const result = await api.dashboard.getSupplierTableData.query({
|
|
2120
|
-
page: opts.page ?? 1,
|
|
2121
|
-
perPage: opts.perPage ?? 20,
|
|
2122
|
-
sort: [],
|
|
2123
|
-
name: opts.name,
|
|
2124
|
-
timeFrame: (opts.timeFrame ?? "ALL"),
|
|
2125
|
-
});
|
|
2126
|
-
out(result);
|
|
2127
|
-
}
|
|
2128
|
-
// --- Dashboard / analytics ---
|
|
2129
|
-
export async function createUser(input) {
|
|
2130
|
-
const user = await api.user.create.mutate(input);
|
|
2131
|
-
out(user);
|
|
2132
|
-
}
|
|
2133
|
-
export async function createApiKeyForUser(input) {
|
|
2134
|
-
const apiKey = await api.mcpApiKey.create.mutate(input);
|
|
2135
|
-
out(apiKey);
|
|
2136
|
-
}
|
|
2137
|
-
export async function listApiKeysForUser(input) {
|
|
2138
|
-
const apiKeys = await api.mcpApiKey.list.query(input);
|
|
2139
|
-
out(apiKeys);
|
|
2140
|
-
}
|
|
2141
|
-
export async function revokeApiKeyForUser(input) {
|
|
2142
|
-
const result = await api.mcpApiKey.revoke.mutate(input);
|
|
2143
|
-
out(result);
|
|
2144
|
-
}
|
|
2145
|
-
export async function listUsers() {
|
|
2146
|
-
const users = await api.user.getAllUsers.query();
|
|
2147
|
-
out(users);
|
|
2148
|
-
}
|
|
2149
|
-
export async function whoAmI() {
|
|
2150
|
-
const user = await api.user.getCurrentUser.query();
|
|
2151
|
-
out(user);
|
|
2152
|
-
}
|
|
2153
|
-
export async function getUserPerformance(userId) {
|
|
2154
|
-
const result = await api.dashboard.getUserPerformance.query({ userId });
|
|
2155
|
-
out(result);
|
|
2156
|
-
}
|
|
2157
|
-
export async function getDashboard(input) {
|
|
2158
|
-
const result = await api.dashboard.getDashboard.query({
|
|
2159
|
-
userId: input.userId,
|
|
2160
|
-
role: input.role ?? "ALL",
|
|
2161
|
-
deals: input.deals ?? "ALL",
|
|
2162
|
-
timeFrame: input.timeFrame ?? "ALL",
|
|
2163
|
-
startDate: input.startDate,
|
|
2164
|
-
endDate: input.endDate,
|
|
2165
|
-
});
|
|
2166
|
-
out(result);
|
|
2167
|
-
}
|
|
2168
|
-
export async function getMonthlyMetrics(input) {
|
|
2169
|
-
const result = await api.dashboard.getMonthlyMetrics.query({
|
|
2170
|
-
userId: input.userId,
|
|
2171
|
-
role: input.role ?? "ALL",
|
|
2172
|
-
deals: input.deals ?? "ALL",
|
|
2173
|
-
timeFrame: input.timeFrame ?? "ALL",
|
|
2174
|
-
startDate: input.startDate,
|
|
2175
|
-
endDate: input.endDate,
|
|
2176
|
-
});
|
|
2177
|
-
out(result);
|
|
2178
|
-
}
|
|
2179
|
-
export async function getSystemOverview(input) {
|
|
2180
|
-
const result = await api.dashboard.getSystemOverview.query({
|
|
2181
|
-
userId: input.userId,
|
|
2182
|
-
role: input.role ?? "ALL",
|
|
2183
|
-
deals: input.deals ?? "ALL",
|
|
2184
|
-
timeFrame: input.timeFrame ?? "ALL",
|
|
2185
|
-
startDate: input.startDate,
|
|
2186
|
-
endDate: input.endDate,
|
|
2187
|
-
});
|
|
2188
|
-
out(result);
|
|
2189
|
-
}
|
|
2190
|
-
export async function getEstimatePerformance(input) {
|
|
2191
|
-
const result = await api.dashboard.getEstimatePerformance.query({
|
|
2192
|
-
userId: input.userId,
|
|
2193
|
-
role: input.role ?? "ALL",
|
|
2194
|
-
deals: input.deals ?? "ALL",
|
|
2195
|
-
timeFrame: input.timeFrame ?? "ALL",
|
|
2196
|
-
startDate: input.startDate,
|
|
2197
|
-
endDate: input.endDate,
|
|
2198
|
-
});
|
|
2199
|
-
out(result);
|
|
2200
|
-
}
|
|
2201
|
-
export async function getFinancialOverview(input) {
|
|
2202
|
-
const result = await api.dashboard.getFinancialOverview.query({
|
|
2203
|
-
userId: input.userId,
|
|
2204
|
-
role: input.role ?? BudgetRole.ALL,
|
|
2205
|
-
deals: input.deals ?? Deals.ALL,
|
|
2206
|
-
timeFrame: input.timeFrame ?? TimeFrame.ALL,
|
|
2207
|
-
startDate: input.startDate,
|
|
2208
|
-
endDate: input.endDate,
|
|
2209
|
-
});
|
|
2210
|
-
out(result);
|
|
2211
|
-
}
|
|
2212
|
-
// --- Error logs ---
|
|
2213
|
-
export async function getRecentErrors(opts) {
|
|
2214
|
-
const result = await api.errorLog.getRecentErrors.query({
|
|
2215
|
-
page: opts.page ?? 1,
|
|
2216
|
-
perPage: opts.perPage ?? 10,
|
|
2217
|
-
severity: opts.severity,
|
|
2218
|
-
status: opts.status,
|
|
2219
|
-
});
|
|
2220
|
-
out(result);
|
|
2221
|
-
}
|
|
2222
|
-
export async function getErrorMetrics() {
|
|
2223
|
-
const result = await api.errorLog.getErrorMetrics.query({});
|
|
2224
|
-
out(result);
|
|
2225
|
-
}
|
|
2226
|
-
// --- Historical / benchmarks ---
|
|
2227
|
-
export async function getApprovedBudgets(opts) {
|
|
2228
|
-
const perPage = Math.min(opts.limit ?? 50, 100);
|
|
2229
|
-
const { budgets, totalCount } = await api.budget.getAllBudgets.query({
|
|
2230
|
-
projectId: opts.projectId,
|
|
2231
|
-
statuses: [
|
|
2232
|
-
ExtendedBudgetStatus.APPROVED,
|
|
2233
|
-
ExtendedBudgetStatus.ESTIMATE_ACCEPTED,
|
|
2234
|
-
ExtendedBudgetStatus.ESTIMATE_CLOSED,
|
|
2235
|
-
],
|
|
2236
|
-
page: 1,
|
|
2237
|
-
perPage,
|
|
2238
|
-
includeDetails: false,
|
|
2239
|
-
});
|
|
2240
|
-
const items = (budgets ?? []).map((b) => {
|
|
2241
|
-
const calc = b;
|
|
2242
|
-
const revenue = calc.totalSellingAfterDiscount ?? 0;
|
|
2243
|
-
const cost = calc.totalCostWithoutGst ?? 0;
|
|
2244
|
-
const gp = calc.gpPercentage ?? 0;
|
|
2245
|
-
const startDate = b.project.startDate;
|
|
2246
|
-
const date = startDate instanceof Date
|
|
2247
|
-
? startDate.toISOString()
|
|
2248
|
-
: String(startDate ?? "");
|
|
2249
|
-
const createdAt = b.createdAt instanceof Date
|
|
2250
|
-
? b.createdAt.toISOString()
|
|
2251
|
-
: String(b.createdAt);
|
|
2252
|
-
return {
|
|
2253
|
-
id: b.id,
|
|
2254
|
-
name: b.name,
|
|
2255
|
-
categoryId: b.categoryId,
|
|
2256
|
-
company: b.project?.company?.name,
|
|
2257
|
-
date,
|
|
2258
|
-
pax: b.project.pax,
|
|
2259
|
-
revenue: Number(revenue.toFixed(2)),
|
|
2260
|
-
cost: Number(cost.toFixed(2)),
|
|
2261
|
-
gpPercentage: Number(gp.toFixed(2)),
|
|
2262
|
-
createdAt,
|
|
2263
|
-
};
|
|
2264
|
-
});
|
|
2265
|
-
let filtered = items;
|
|
2266
|
-
if (opts.minRevenue != null) {
|
|
2267
|
-
const minR = opts.minRevenue;
|
|
2268
|
-
filtered = filtered.filter((i) => i.revenue >= minR);
|
|
2269
|
-
}
|
|
2270
|
-
if (opts.maxRevenue != null) {
|
|
2271
|
-
const maxR = opts.maxRevenue;
|
|
2272
|
-
filtered = filtered.filter((i) => i.revenue <= maxR);
|
|
2273
|
-
}
|
|
2274
|
-
out({ budgets: filtered, count: filtered.length, totalCount });
|
|
2275
|
-
}
|
|
2276
|
-
export async function getBudgetCategoryBenchmarks(opts) {
|
|
2277
|
-
const result = await api.budget.getBudgetCategoryBenchmarks.query({
|
|
2278
|
-
categoryId: opts.categoryId,
|
|
2279
|
-
limit: opts.limit ?? 100,
|
|
2280
|
-
});
|
|
2281
|
-
out(result);
|
|
2282
|
-
}
|
|
2283
|
-
export async function getItemPricingHistory(opts) {
|
|
2284
|
-
const result = await api.budget.getItemPricingHistory.query({
|
|
2285
|
-
itemName: opts.itemName,
|
|
2286
|
-
categoryId: opts.categoryId,
|
|
2287
|
-
supplierId: opts.supplierId,
|
|
2288
|
-
limit: opts.limit ?? 100,
|
|
2289
|
-
});
|
|
2290
|
-
out(result);
|
|
2291
|
-
}
|
|
2292
|
-
export async function getSupplierPricingHistory(opts) {
|
|
2293
|
-
const result = await api.budget.getSupplierPricingHistory.query({
|
|
2294
|
-
supplierId: opts.supplierId,
|
|
2295
|
-
itemCategoryId: opts.itemCategoryId,
|
|
2296
|
-
limit: opts.limit ?? 100,
|
|
2297
|
-
});
|
|
2298
|
-
out(result);
|
|
2299
|
-
}
|