node-autotask 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/MCP_TOOL_CATALOG.json +44 -8
- package/MCP_TOOL_MANIFEST.md +13 -11
- package/N8N_MCP_MANIFEST.json +55 -5
- package/N8N_MCP_MANIFEST.md +9 -7
- package/README.md +1 -1
- package/capabilities.json +1607 -1305
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-F7FVOXPQ.js → chunk-6EQSXYRW.js} +1606 -1304
- package/dist/{chunk-ZJ4ONAQF.cjs → chunk-D5IVS6GY.cjs} +4 -4
- package/dist/{chunk-UHTZLUDE.cjs → chunk-GUY2KK7U.cjs} +143 -143
- package/dist/{chunk-42ZUISDH.js → chunk-IRVZGW5K.js} +1 -1
- package/dist/{chunk-GURC52J7.js → chunk-O2D7KDDD.js} +2 -2
- package/dist/{chunk-ZGTFVZL5.js → chunk-SYX6EWMT.js} +287 -78
- package/dist/{chunk-5GGCSRT4.cjs → chunk-WYKGD5UD.cjs} +795 -586
- package/dist/{chunk-5CC7LGNM.cjs → chunk-ZEGMVEF3.cjs} +1606 -1304
- package/dist/{index-D4XYtn4s.d.cts → index-CORcZUNm.d.cts} +79 -2
- package/dist/{index-BIWy9m-a.d.ts → index-DXuV04zM.d.ts} +79 -2
- package/dist/index.cjs +22 -22
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/operations/index.cjs +112 -112
- package/dist/operations/index.d.cts +2 -2
- package/dist/operations/index.d.ts +2 -2
- package/dist/operations/index.js +2 -2
- package/dist/pool.cjs +9 -9
- package/dist/pool.d.cts +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +3 -3
- package/docs/API.md +40 -2
- package/package.json +1 -1
|
@@ -38,10 +38,10 @@ import {
|
|
|
38
38
|
unwrapItem,
|
|
39
39
|
unwrapItems,
|
|
40
40
|
unwrapOperation
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-IRVZGW5K.js";
|
|
42
42
|
import {
|
|
43
43
|
CAPABILITIES
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-6EQSXYRW.js";
|
|
45
45
|
|
|
46
46
|
// src/resources/appointments.ts
|
|
47
47
|
var AppointmentsResource = class extends BaseResource {
|
|
@@ -1291,6 +1291,82 @@ async function upsertByName(resource, companyName, params, opts, http) {
|
|
|
1291
1291
|
return createWithDiscovery(resource, body, owner, caveat, opts);
|
|
1292
1292
|
}
|
|
1293
1293
|
|
|
1294
|
+
// src/resources/special/_shared/ticketStatus.ts
|
|
1295
|
+
var ASSUMED_CLOSED_STATUS_ID = 5;
|
|
1296
|
+
var CLOSED_STATUS_LABELS = ["complete", "completed", "closed", "cancelled", "canceled", "resolved"];
|
|
1297
|
+
var OP = "tickets.statusSemantics";
|
|
1298
|
+
function picklistOf(fields) {
|
|
1299
|
+
if (!Array.isArray(fields)) return [];
|
|
1300
|
+
const status = fields.find((f) => typeof f?.name === "string" && f.name.toLowerCase() === "status");
|
|
1301
|
+
if (status === void 0 || status.isPickList !== true || !Array.isArray(status.picklistValues)) return [];
|
|
1302
|
+
const out = [];
|
|
1303
|
+
for (const v of status.picklistValues) {
|
|
1304
|
+
const id = Number(v?.value);
|
|
1305
|
+
if (!Number.isSafeInteger(id)) continue;
|
|
1306
|
+
out.push({ id, label: typeof v.label === "string" ? v.label : "" });
|
|
1307
|
+
}
|
|
1308
|
+
return out;
|
|
1309
|
+
}
|
|
1310
|
+
async function readFields(http, fresh) {
|
|
1311
|
+
const cache = http.fieldCache;
|
|
1312
|
+
const key = "tickets";
|
|
1313
|
+
if (!fresh && cache !== void 0) {
|
|
1314
|
+
const shared = cache.inflight(key, "fields");
|
|
1315
|
+
if (shared !== void 0) return await shared;
|
|
1316
|
+
const hit = cache.get(key, "fields");
|
|
1317
|
+
if (hit !== void 0) return hit;
|
|
1318
|
+
}
|
|
1319
|
+
const res = await http.request({ method: "GET", path: "/V1.0/Tickets/entityInformation/fields", operation: OP, maxRetries: 0 });
|
|
1320
|
+
const fields = res?.fields;
|
|
1321
|
+
if (!Array.isArray(fields)) {
|
|
1322
|
+
throw new AutotaskError("Expected the FieldInformationResultModel envelope { fields: [] }; got a different shape", { code: "INVALID_RESPONSE", category: "server" });
|
|
1323
|
+
}
|
|
1324
|
+
cache?.set(key, "fields", fields);
|
|
1325
|
+
return fields;
|
|
1326
|
+
}
|
|
1327
|
+
function assumed(excludeStatusIds) {
|
|
1328
|
+
const closedIds = excludeStatusIds === void 0 ? [ASSUMED_CLOSED_STATUS_ID] : [...excludeStatusIds];
|
|
1329
|
+
return {
|
|
1330
|
+
picklist: [],
|
|
1331
|
+
closedIds,
|
|
1332
|
+
source: "assumed",
|
|
1333
|
+
assumedId: ASSUMED_CLOSED_STATUS_ID,
|
|
1334
|
+
note: `closed = status ${closedIds.join(", ")} \u2014 ASSUMED, not read from the tenant: the Tickets status picklist could not be resolved, so the historical id ${ASSUMED_CLOSED_STATUS_ID} stands in (fix: expose the Tickets field catalog to this API user, or pass excludeStatusIds)`
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
async function resolveTicketStatusSemantics(http, opts = {}) {
|
|
1338
|
+
if (opts.excludeStatusIds !== void 0) {
|
|
1339
|
+
const closedIds2 = [...new Set(opts.excludeStatusIds.map((id) => Number(id)))];
|
|
1340
|
+
if (closedIds2.length === 0 || closedIds2.some((id) => !Number.isSafeInteger(id) || id < 1)) {
|
|
1341
|
+
throw new AutotaskConfigError(`excludeStatusIds must be a non-empty list of positive status ids (got ${JSON.stringify(opts.excludeStatusIds)})`, { operation: OP });
|
|
1342
|
+
}
|
|
1343
|
+
return { picklist: [], closedIds: closedIds2, source: "picklist", note: `closed = status ${closedIds2.join(", ")} \u2014 supplied by the caller` };
|
|
1344
|
+
}
|
|
1345
|
+
let fields;
|
|
1346
|
+
try {
|
|
1347
|
+
fields = await readFields(http, opts.fresh === true);
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
if (e instanceof AutotaskError && e.code === "ABORTED") throw e;
|
|
1350
|
+
return assumed();
|
|
1351
|
+
}
|
|
1352
|
+
const picklist = picklistOf(fields);
|
|
1353
|
+
const closedIds = picklist.filter((v) => CLOSED_STATUS_LABELS.includes(v.label.trim().toLowerCase())).map((v) => v.id);
|
|
1354
|
+
if (closedIds.length === 0) return assumed();
|
|
1355
|
+
const labels = picklist.filter((v) => closedIds.includes(v.id)).map((v) => `${v.id} ${v.label}`).join(", ");
|
|
1356
|
+
return {
|
|
1357
|
+
picklist,
|
|
1358
|
+
closedIds,
|
|
1359
|
+
source: "picklist",
|
|
1360
|
+
note: `closed = status not in [${closedIds.join(", ")}] (${labels}) \u2014 resolved from the tenant's Tickets status picklist`
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function openStatusFilter(closedIds) {
|
|
1364
|
+
return closedIds.length === 1 ? { field: "status", op: "noteq", value: closedIds[0] } : { field: "status", op: "notIn", value: [...closedIds] };
|
|
1365
|
+
}
|
|
1366
|
+
function isClosedStatus(status, closedIds) {
|
|
1367
|
+
return typeof status === "number" && closedIds.includes(status);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1294
1370
|
// src/resources/special/companies/fullProfile.ts
|
|
1295
1371
|
var FULL_PROFILE_PROJECTABLE_PARTS = ["locations", "contacts", "notes", "projects", "openTickets"];
|
|
1296
1372
|
var PART_ENTITY = {
|
|
@@ -1300,7 +1376,7 @@ var PART_ENTITY = {
|
|
|
1300
1376
|
projects: "projects",
|
|
1301
1377
|
openTickets: "tickets"
|
|
1302
1378
|
};
|
|
1303
|
-
var OPEN_TICKETS_NOTE = "open = status != 5 (Complete) \u2014 the
|
|
1379
|
+
var OPEN_TICKETS_NOTE = "open = status != 5 (Complete) \u2014 ASSUMED: the tenant Tickets status picklist could not be read, so id 5 is the fallback closed id; tickets in any other status count as open";
|
|
1304
1380
|
var PROFILE_PART_LIMIT = 50;
|
|
1305
1381
|
var LOCATION_LIMIT_MAX = 500;
|
|
1306
1382
|
function locationLimit(value) {
|
|
@@ -1361,6 +1437,8 @@ async function scopedCompanyQuery(http, path, companyID, limit, includeFields) {
|
|
|
1361
1437
|
return project(unwrapItems(res).items, includeFields);
|
|
1362
1438
|
}
|
|
1363
1439
|
async function readOpenTickets(http, companyID, ticketLimit, includeFields) {
|
|
1440
|
+
const semantics = await resolveTicketStatusSemantics(http);
|
|
1441
|
+
const openLeaf = openStatusFilter(semantics.closedIds);
|
|
1364
1442
|
const openRows = await (async () => {
|
|
1365
1443
|
const res = await http.request({
|
|
1366
1444
|
method: "POST",
|
|
@@ -1368,30 +1446,31 @@ async function readOpenTickets(http, companyID, ticketLimit, includeFields) {
|
|
|
1368
1446
|
path: "/V1.0/Tickets/query",
|
|
1369
1447
|
body: {
|
|
1370
1448
|
MaxRecords: ticketLimit,
|
|
1371
|
-
filter: [{ field: "companyID", op: "eq", value: companyID },
|
|
1449
|
+
filter: [{ field: "companyID", op: "eq", value: companyID }, openLeaf],
|
|
1372
1450
|
...includeFields ? { IncludeFields: includeFields } : {}
|
|
1373
1451
|
},
|
|
1374
1452
|
operation: "companies.fullProfile"
|
|
1375
1453
|
});
|
|
1376
1454
|
return project(unwrapItems(res).items, includeFields);
|
|
1377
1455
|
})();
|
|
1456
|
+
const closedStatus = { closedIds: semantics.closedIds, source: semantics.source, note: semantics.note };
|
|
1457
|
+
const partNote = semantics.source === "assumed" ? OPEN_TICKETS_NOTE : semantics.note;
|
|
1378
1458
|
let openTickets = {
|
|
1379
1459
|
items: openRows,
|
|
1380
1460
|
capped: openRows.length >= ticketLimit,
|
|
1381
1461
|
blind: false,
|
|
1382
|
-
note:
|
|
1462
|
+
note: partNote,
|
|
1463
|
+
closedStatus
|
|
1383
1464
|
};
|
|
1384
1465
|
if (openRows.length === 0) {
|
|
1385
|
-
const stamp = blindStampFor(BLIND_SURFACE_STAMPS["tickets"]?.query, [
|
|
1386
|
-
{ field: "companyID", op: "eq", value: companyID },
|
|
1387
|
-
{ field: "status", op: "noteq", value: 5 }
|
|
1388
|
-
]);
|
|
1466
|
+
const stamp = blindStampFor(BLIND_SURFACE_STAMPS["tickets"]?.query, [{ field: "companyID", op: "eq", value: companyID }, openLeaf]);
|
|
1389
1467
|
if (stamp !== void 0) {
|
|
1390
1468
|
openTickets = {
|
|
1391
1469
|
items: openRows,
|
|
1392
1470
|
capped: openRows.length >= ticketLimit,
|
|
1393
1471
|
blind: true,
|
|
1394
|
-
note: `${stamp.note} ${
|
|
1472
|
+
note: `${stamp.note} ${partNote}`,
|
|
1473
|
+
closedStatus,
|
|
1395
1474
|
blindSurface: stamp
|
|
1396
1475
|
};
|
|
1397
1476
|
}
|
|
@@ -1589,7 +1668,7 @@ function validateUnbilledWorkOptions(companyId, opts) {
|
|
|
1589
1668
|
}
|
|
1590
1669
|
|
|
1591
1670
|
// src/resources/special/companies/unbilledWork.ts
|
|
1592
|
-
var
|
|
1671
|
+
var OP2 = "companies.unbilledWork";
|
|
1593
1672
|
function capJoinSet(ids2, cap) {
|
|
1594
1673
|
const sorted = [...new Set(ids2)].sort((a, b) => a - b);
|
|
1595
1674
|
if (sorted.length <= cap) return { ids: sorted, truncated: false };
|
|
@@ -1626,14 +1705,14 @@ async function readCompanyAndParent(resource, id, includeParent) {
|
|
|
1626
1705
|
}
|
|
1627
1706
|
async function readJoinSets(http, companyIds, v) {
|
|
1628
1707
|
const ticketsCond = [{ field: "companyID", op: "in", value: companyIds }];
|
|
1629
|
-
const ticketsRaw = await boundedPart(http, "/V1.0/Tickets/query", ticketsCond, v.limit,
|
|
1708
|
+
const ticketsRaw = await boundedPart(http, "/V1.0/Tickets/query", ticketsCond, v.limit, OP2);
|
|
1630
1709
|
const projectsCond = [{ field: "companyID", op: "in", value: companyIds }];
|
|
1631
|
-
const projectsRaw = await boundedPart(http, "/V1.0/Projects/query", projectsCond, v.limit,
|
|
1710
|
+
const projectsRaw = await boundedPart(http, "/V1.0/Projects/query", projectsCond, v.limit, OP2);
|
|
1632
1711
|
const projectIds = projectsRaw.items.map((p) => num(p.id)).filter((n) => n > 0);
|
|
1633
1712
|
let taskIds = [];
|
|
1634
1713
|
let tasksHasMore = false;
|
|
1635
1714
|
if (projectIds.length > 0) {
|
|
1636
|
-
const tasksRaw = await boundedPart(http, "/V1.0/Tasks/query", [{ field: "projectID", op: "in", value: projectIds }], v.limit,
|
|
1715
|
+
const tasksRaw = await boundedPart(http, "/V1.0/Tasks/query", [{ field: "projectID", op: "in", value: projectIds }], v.limit, OP2);
|
|
1637
1716
|
taskIds = tasksRaw.items.map((t) => num(t.id)).filter((n) => n > 0);
|
|
1638
1717
|
tasksHasMore = tasksRaw.hasMore;
|
|
1639
1718
|
}
|
|
@@ -1657,7 +1736,7 @@ async function readBillingPart(http, v, companyIds) {
|
|
|
1657
1736
|
}
|
|
1658
1737
|
const billingFields = resolveFieldsPreset("billing_join", "billing_items");
|
|
1659
1738
|
const billingIncludeFields = [...billingFields, "id", ...v.includeNotes ? ["description"] : []];
|
|
1660
|
-
const billingRaw = await boundedPart(http, "/V1.0/BillingItems/query", billingConditions, v.limit,
|
|
1739
|
+
const billingRaw = await boundedPart(http, "/V1.0/BillingItems/query", billingConditions, v.limit, OP2, billingIncludeFields);
|
|
1661
1740
|
let billingItems = billingRaw.items;
|
|
1662
1741
|
let billingNoteCharsTruncated = 0;
|
|
1663
1742
|
if (v.includeNotes) {
|
|
@@ -1701,7 +1780,7 @@ async function readTimePart(http, v, join) {
|
|
|
1701
1780
|
if (v.dateFrom !== void 0) timeConditions.push({ field: "dateWorked", op: "gte", value: v.dateFrom });
|
|
1702
1781
|
if (v.dateTo !== void 0) timeConditions.push({ field: "dateWorked", op: "lte", value: v.dateTo });
|
|
1703
1782
|
const timeFields = ["taskID", "ticketID", "hoursWorked", "dateWorked", "id", ...v.includeNotes ? ["summaryNotes"] : []];
|
|
1704
|
-
const raw = await boundedPart(http, "/V1.0/TimeEntries/query", timeConditions, v.limit,
|
|
1783
|
+
const raw = await boundedPart(http, "/V1.0/TimeEntries/query", timeConditions, v.limit, OP2, timeFields);
|
|
1705
1784
|
entries = raw.items;
|
|
1706
1785
|
let cappedItems = entries;
|
|
1707
1786
|
let noteCharsTruncated = 0;
|
|
@@ -5220,7 +5299,7 @@ function appendWiresNote(wires, note) {
|
|
|
5220
5299
|
wires.note = (wires.note ? wires.note + " | " : "") + note;
|
|
5221
5300
|
}
|
|
5222
5301
|
function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
5223
|
-
const
|
|
5302
|
+
const OP9 = PROFITABILITY_OP;
|
|
5224
5303
|
const id = parseIdentifier(contractId, "contractId");
|
|
5225
5304
|
const idNum = typeof id === "number" ? id : Number(id);
|
|
5226
5305
|
const wires = { issued: 0, requests: 0, budget };
|
|
@@ -5232,19 +5311,19 @@ function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
|
5232
5311
|
} };
|
|
5233
5312
|
const readPart = (path, conditions, fields) => {
|
|
5234
5313
|
wires.issued += 1;
|
|
5235
|
-
return degradable(() => boundedPart(http, path, conditions, v.limit,
|
|
5314
|
+
return degradable(() => boundedPart(http, path, conditions, v.limit, OP9, fields));
|
|
5236
5315
|
};
|
|
5237
5316
|
const truncatedMonths = {};
|
|
5238
5317
|
const windowedPart = async (name, key, path, cond, fields, dateField) => {
|
|
5239
5318
|
const first = await readPart(path, cond, fields);
|
|
5240
5319
|
if (first.absent !== void 0 || !first.hasMore) return { part: stampPart(key, first, cond), rows: first.items };
|
|
5241
|
-
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit,
|
|
5320
|
+
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit, OP9, fields, dateField, v.dateFrom, v.dateTo, wires));
|
|
5242
5321
|
if (!("truncatedMonths" in wp)) return { part: stampPart(key, wp, cond), rows: [] };
|
|
5243
5322
|
const { truncatedMonths: tm, ...part } = wp;
|
|
5244
5323
|
truncatedMonths[name] = tm;
|
|
5245
5324
|
return { part, rows: wp.items };
|
|
5246
5325
|
};
|
|
5247
|
-
return { v, OP:
|
|
5326
|
+
return { v, OP: OP9, id, idNum, rawHttp, http, wires, windowOn, readPart, windowedPart, truncatedMonths };
|
|
5248
5327
|
}
|
|
5249
5328
|
|
|
5250
5329
|
// src/resources/special/contracts/profitability.stages.ts
|
|
@@ -10434,7 +10513,6 @@ var TicketChangeRequestApprovalsResource = class extends BaseResource {
|
|
|
10434
10513
|
// src/resources/special/_shared/move-engine.ts
|
|
10435
10514
|
import { randomUUID } from "crypto";
|
|
10436
10515
|
var MOVE_SOURCE_AGE_DEFAULT_DAYS = 14;
|
|
10437
|
-
var TICKET_COMPLETE_STATUS = 5;
|
|
10438
10516
|
var MOVE_EFFECTS_NOTE = "Create then delete: the new record gets a fresh create date and creator; creating/deleting may trigger notifications, SLA and workflow rules the API cannot suppress; vendor acceptance of the create is not validated by a dry run";
|
|
10439
10517
|
var refuse = (code, message, ids2, operation, extra = {}) => new MoveRefusedError(message, { code, operation, details: { status: "failed", ...ids2, ...extra } });
|
|
10440
10518
|
async function queryRows(http, resource, filter, operation, retries) {
|
|
@@ -10487,11 +10565,19 @@ async function preview(http, spec, sourceId, toTicketId, opts) {
|
|
|
10487
10565
|
}
|
|
10488
10566
|
const target = await readTicket(http, toTicketId, opts.maxRetries, op);
|
|
10489
10567
|
if (target === null) throw refuse("MOVE_TARGET_MISSING", `${op}: target ticket ${toTicketId} does not exist`, ids2, op);
|
|
10490
|
-
|
|
10491
|
-
|
|
10568
|
+
const closedStatus = await resolveTicketStatusSemantics(http);
|
|
10569
|
+
if (isClosedStatus(target.status, closedStatus.closedIds) || target.completedDate !== void 0 && target.completedDate !== null) {
|
|
10570
|
+
throw refuse(
|
|
10571
|
+
"MOVE_TARGET_CLOSED",
|
|
10572
|
+
`${op}: target ticket ${toTicketId} is closed (status in [${closedStatus.closedIds.join(", ")}] or completed) \u2014 closed ids ${closedStatus.source === "assumed" ? "ASSUMED (the tenant status picklist could not be read)" : "resolved from the tenant Tickets status picklist"}`,
|
|
10573
|
+
ids2,
|
|
10574
|
+
op,
|
|
10575
|
+
{ closedStatusIds: closedStatus.closedIds, closedStatusSource: closedStatus.source }
|
|
10576
|
+
);
|
|
10492
10577
|
}
|
|
10493
10578
|
const origin = await readTicket(http, sourceTicketId, opts.maxRetries, op);
|
|
10494
10579
|
const facts = {
|
|
10580
|
+
closedStatus: { closedIds: closedStatus.closedIds, source: closedStatus.source, note: closedStatus.note },
|
|
10495
10581
|
sourceTicketId,
|
|
10496
10582
|
sourceCompanyID: origin?.companyID ?? null,
|
|
10497
10583
|
targetCompanyID: target.companyID ?? null,
|
|
@@ -10605,22 +10691,22 @@ function uiLinkFor(http, entity, id) {
|
|
|
10605
10691
|
}
|
|
10606
10692
|
|
|
10607
10693
|
// src/resources/special/ticket_charges.ts
|
|
10608
|
-
var
|
|
10694
|
+
var OP3 = "ticket_charges.moveToTicket";
|
|
10609
10695
|
var COPY = ["name", "description", "chargeType", "billingCodeID", "productID", "unitQuantity", "unitCost", "unitPrice", "datePurchased", "isBillableToCompany", "notes", "purchaseOrderNumber", "internalPurchaseOrderNumber"];
|
|
10610
10696
|
var CONTRACT_SCOPED = ["contractServiceID", "contractServiceBundleID"];
|
|
10611
10697
|
async function readCharge(http, id, retries) {
|
|
10612
|
-
const rows = await queryRows(http, "TicketCharges", [{ field: "id", op: "eq", value: id }],
|
|
10698
|
+
const rows = await queryRows(http, "TicketCharges", [{ field: "id", op: "eq", value: id }], OP3, retries);
|
|
10613
10699
|
return rows[0] ?? null;
|
|
10614
10700
|
}
|
|
10615
10701
|
async function checkSource({ http, source, sourceId, sourceTicketId, toTicketId }) {
|
|
10616
10702
|
if (source.isBilled === true) {
|
|
10617
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
10703
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP3}: ticket charge ${sourceId} is billed \u2014 move it in the Autotask UI`, { sourceId, toTicketId }, OP3, {
|
|
10618
10704
|
manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "tickets", sourceTicketId), reason: "billed charges are not moved by the API (the ticket link opens its charges)" }
|
|
10619
10705
|
});
|
|
10620
10706
|
}
|
|
10621
10707
|
}
|
|
10622
10708
|
var SPEC = {
|
|
10623
|
-
operation:
|
|
10709
|
+
operation: OP3,
|
|
10624
10710
|
entity: "ticket_charge",
|
|
10625
10711
|
readSource: readCharge,
|
|
10626
10712
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -12061,6 +12147,121 @@ async function fullContext(resource, id, opts = {}, http) {
|
|
|
12061
12147
|
return result;
|
|
12062
12148
|
}
|
|
12063
12149
|
|
|
12150
|
+
// src/resources/special/tickets/forResource.ts
|
|
12151
|
+
var OP4 = "tickets.forResource";
|
|
12152
|
+
var TICKET_CHUNK = 100;
|
|
12153
|
+
var LIMIT_DEFAULT = 100;
|
|
12154
|
+
var LIMIT_MAX = 500;
|
|
12155
|
+
var LINK_READ_CAP = 500;
|
|
12156
|
+
var TICKET_FIELDS2 = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
12157
|
+
function positiveId(value, name) {
|
|
12158
|
+
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
12159
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
12160
|
+
throw new AutotaskConfigError(`${OP4}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP4 });
|
|
12161
|
+
}
|
|
12162
|
+
return n;
|
|
12163
|
+
}
|
|
12164
|
+
async function ticketPage(http, filter, maxRecords, fields) {
|
|
12165
|
+
const res = await http.request({
|
|
12166
|
+
method: "POST",
|
|
12167
|
+
idempotent: true,
|
|
12168
|
+
path: "/V1.0/Tickets/query",
|
|
12169
|
+
body: { MaxRecords: maxRecords, filter, IncludeFields: fields },
|
|
12170
|
+
operation: OP4
|
|
12171
|
+
});
|
|
12172
|
+
const page = unwrapItems(res, OP4);
|
|
12173
|
+
return { rows: page.items, more: page.pageDetails.nextPageUrl !== null };
|
|
12174
|
+
}
|
|
12175
|
+
async function forResource(_resource, resourceIDIn, http, opts = {}) {
|
|
12176
|
+
if (opts === null || typeof opts !== "object") throw new AutotaskConfigError(`${OP4}: opts must be an object`, { operation: OP4 });
|
|
12177
|
+
const resourceID = positiveId(resourceIDIn, "resourceID");
|
|
12178
|
+
const scope = opts.scope ?? "both";
|
|
12179
|
+
if (scope !== "primary" && scope !== "secondary" && scope !== "both") {
|
|
12180
|
+
throw new AutotaskConfigError(`${OP4}: scope must be 'primary', 'secondary' or 'both' (got ${JSON.stringify(opts.scope)})`, { operation: OP4 });
|
|
12181
|
+
}
|
|
12182
|
+
if (opts.openOnly !== void 0 && typeof opts.openOnly !== "boolean") {
|
|
12183
|
+
throw new AutotaskConfigError(`${OP4}: openOnly must be a boolean`, { operation: OP4 });
|
|
12184
|
+
}
|
|
12185
|
+
const limit = opts.limit === void 0 ? LIMIT_DEFAULT : opts.limit;
|
|
12186
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > LIMIT_MAX) {
|
|
12187
|
+
throw new AutotaskConfigError(`${OP4}: limit must be an integer 1-${LIMIT_MAX} (got ${String(opts.limit)})`, { operation: OP4 });
|
|
12188
|
+
}
|
|
12189
|
+
const includeFields = opts.includeFields ?? [];
|
|
12190
|
+
if (!Array.isArray(includeFields) || includeFields.some((f) => typeof f !== "string" || f.length === 0)) {
|
|
12191
|
+
throw new AutotaskConfigError(`${OP4}: includeFields must be a list of field names`, { operation: OP4 });
|
|
12192
|
+
}
|
|
12193
|
+
const openOnly = opts.openOnly === true;
|
|
12194
|
+
const fields = [.../* @__PURE__ */ new Set([...TICKET_FIELDS2, ...includeFields])];
|
|
12195
|
+
const closed = await resolveTicketStatusSemantics(http, opts.excludeStatusIds === void 0 ? {} : { excludeStatusIds: opts.excludeStatusIds });
|
|
12196
|
+
const closedStatus = { closedIds: closed.closedIds, source: closed.source, note: closed.note };
|
|
12197
|
+
const openLeaf = openOnly ? [openStatusFilter(closed.closedIds)] : [];
|
|
12198
|
+
const linkIds = [];
|
|
12199
|
+
let capped = false;
|
|
12200
|
+
if (scope !== "primary") {
|
|
12201
|
+
const res = await http.request({
|
|
12202
|
+
method: "POST",
|
|
12203
|
+
idempotent: true,
|
|
12204
|
+
path: "/V1.0/TicketSecondaryResources/query",
|
|
12205
|
+
body: { MaxRecords: LINK_READ_CAP, filter: [{ field: "resourceID", op: "eq", value: resourceID }], IncludeFields: ["ticketID"] },
|
|
12206
|
+
operation: OP4
|
|
12207
|
+
});
|
|
12208
|
+
const page = unwrapItems(res, OP4);
|
|
12209
|
+
for (const row of page.items) if (typeof row.ticketID === "number" && !linkIds.includes(row.ticketID)) linkIds.push(row.ticketID);
|
|
12210
|
+
if (page.items.length >= LINK_READ_CAP || page.pageDetails.nextPageUrl !== null) capped = true;
|
|
12211
|
+
}
|
|
12212
|
+
const primary = /* @__PURE__ */ new Map();
|
|
12213
|
+
if (scope !== "secondary") {
|
|
12214
|
+
const page = await ticketPage(http, [{ field: "assignedResourceID", op: "eq", value: resourceID }, ...openLeaf], limit, fields);
|
|
12215
|
+
if (page.more || page.rows.length >= limit) capped = true;
|
|
12216
|
+
for (const t of page.rows) if (typeof t.id === "number") primary.set(t.id, t);
|
|
12217
|
+
}
|
|
12218
|
+
const secondary = /* @__PURE__ */ new Map();
|
|
12219
|
+
const unresolvedIds = [];
|
|
12220
|
+
for (let c = 0; c < linkIds.length; c += TICKET_CHUNK) {
|
|
12221
|
+
const chunk = linkIds.slice(c, c + TICKET_CHUNK);
|
|
12222
|
+
const page = await ticketPage(http, [{ field: "id", op: "in", value: chunk }, ...openLeaf], chunk.length, fields);
|
|
12223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12224
|
+
for (const t of page.rows) {
|
|
12225
|
+
if (typeof t.id !== "number") continue;
|
|
12226
|
+
secondary.set(t.id, t);
|
|
12227
|
+
seen.add(t.id);
|
|
12228
|
+
}
|
|
12229
|
+
for (const id of chunk) if (!seen.has(id)) unresolvedIds.push(id);
|
|
12230
|
+
}
|
|
12231
|
+
const merged = /* @__PURE__ */ new Map();
|
|
12232
|
+
for (const [id, t] of primary) merged.set(id, t);
|
|
12233
|
+
for (const [id, t] of secondary) if (!merged.has(id)) merged.set(id, t);
|
|
12234
|
+
let tickets = [...merged.values()];
|
|
12235
|
+
if (openOnly) tickets = tickets.filter((t) => !isClosedStatus(t.status, closed.closedIds));
|
|
12236
|
+
tickets.sort((a, b) => Number(a.id) - Number(b.id));
|
|
12237
|
+
if (tickets.length > limit) {
|
|
12238
|
+
tickets = tickets.slice(0, limit);
|
|
12239
|
+
capped = true;
|
|
12240
|
+
}
|
|
12241
|
+
const openCount = tickets.filter((t) => !isClosedStatus(t.status, closed.closedIds)).length;
|
|
12242
|
+
const primaryIds = [...primary.keys()].sort((a, b) => a - b);
|
|
12243
|
+
const secondaryIds = [...secondary.keys()].sort((a, b) => a - b);
|
|
12244
|
+
const both = primaryIds.filter((id) => secondary.has(id));
|
|
12245
|
+
return {
|
|
12246
|
+
resourceID,
|
|
12247
|
+
scope,
|
|
12248
|
+
openOnly,
|
|
12249
|
+
tickets,
|
|
12250
|
+
primaryIds,
|
|
12251
|
+
secondaryIds,
|
|
12252
|
+
bothIds: both,
|
|
12253
|
+
counts: { primary: primaryIds.length, secondary: secondaryIds.length, both: both.length, open: openCount },
|
|
12254
|
+
capped,
|
|
12255
|
+
unresolvedIds: unresolvedIds.sort((a, b) => a - b),
|
|
12256
|
+
closedStatus
|
|
12257
|
+
};
|
|
12258
|
+
}
|
|
12259
|
+
|
|
12260
|
+
// src/resources/special/tickets/statusSemantics.ts
|
|
12261
|
+
async function statusSemantics(_resource, opts = {}, http) {
|
|
12262
|
+
return resolveTicketStatusSemantics(http, opts);
|
|
12263
|
+
}
|
|
12264
|
+
|
|
12064
12265
|
// src/resources/tickets.ts
|
|
12065
12266
|
var TicketsResource = class extends BaseResource {
|
|
12066
12267
|
constructor(http, pageSize) {
|
|
@@ -12166,6 +12367,14 @@ var TicketsResource = class extends BaseResource {
|
|
|
12166
12367
|
fullContext(id, opts) {
|
|
12167
12368
|
return fullContext(this, id, opts, this.http);
|
|
12168
12369
|
}
|
|
12370
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
12371
|
+
statusSemantics(opts) {
|
|
12372
|
+
return statusSemantics(this, opts, this.http);
|
|
12373
|
+
}
|
|
12374
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
12375
|
+
forResource(resourceID, opts) {
|
|
12376
|
+
return forResource(this, resourceID, this.http, opts);
|
|
12377
|
+
}
|
|
12169
12378
|
};
|
|
12170
12379
|
|
|
12171
12380
|
// src/resources/ticket_webhooks.ts
|
|
@@ -12294,12 +12503,12 @@ async function postedDateStatusQuery(_resource, mode, http, opts) {
|
|
|
12294
12503
|
return items;
|
|
12295
12504
|
}
|
|
12296
12505
|
async function postedWithin(_resource, from, to, http, opts, opLabel = "time_entries.postedWithin") {
|
|
12297
|
-
const
|
|
12298
|
-
if (opts?.tz !== void 0) validateZone(opts.tz,
|
|
12299
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
12300
|
-
const toB = customRangeDay(to, "to", void 0,
|
|
12506
|
+
const OP9 = opLabel;
|
|
12507
|
+
if (opts?.tz !== void 0) validateZone(opts.tz, OP9);
|
|
12508
|
+
const fromB = customRangeDay(from, "from", void 0, OP9);
|
|
12509
|
+
const toB = customRangeDay(to, "to", void 0, OP9);
|
|
12301
12510
|
if (fromB.ms > toB.ms) {
|
|
12302
|
-
throw new AutotaskConfigError(`${
|
|
12511
|
+
throw new AutotaskConfigError(`${OP9}: from (${iso2(fromB.ms)}) must not be after to (${iso2(toB.ms)})`, { operation: OP9 });
|
|
12303
12512
|
}
|
|
12304
12513
|
const windowToMs = toB.ms + DAY_MS2 - 1;
|
|
12305
12514
|
const filter = [
|
|
@@ -12310,7 +12519,7 @@ async function postedWithin(_resource, from, to, http, opts, opLabel = "time_ent
|
|
|
12310
12519
|
else if (opts?.posted === false) filter.push({ field: "postedDate", op: "notExist" });
|
|
12311
12520
|
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
12312
12521
|
if (opts?.resourceID !== void 0) filter.push({ field: "resourceID", op: "eq", value: opts.resourceID });
|
|
12313
|
-
const { items, hasMore } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit),
|
|
12522
|
+
const { items, hasMore } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), OP9, http);
|
|
12314
12523
|
let postedCount = 0;
|
|
12315
12524
|
for (const item of items) {
|
|
12316
12525
|
const postedDate = item.postedDate;
|
|
@@ -12520,7 +12729,7 @@ async function resolveCode(http, family, familyLabel2, pinnedId, departments, op
|
|
|
12520
12729
|
}
|
|
12521
12730
|
|
|
12522
12731
|
// src/resources/special/time_entries/createForResource.ts
|
|
12523
|
-
var
|
|
12732
|
+
var OP5 = "time_entries.createForResource";
|
|
12524
12733
|
var PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
12525
12734
|
"internallAllocationCodeID",
|
|
12526
12735
|
"ticketID",
|
|
@@ -12556,30 +12765,30 @@ function validateCreateParams(params) {
|
|
|
12556
12765
|
if (pinCount > 1) {
|
|
12557
12766
|
throw new AutotaskConfigError(
|
|
12558
12767
|
"createForResource: exactly one linkage pin is honoured (internallAllocationCodeID | ticketID | taskID) \u2014 the vendor gate requires exactly one; pass one pin (or none for the default internal linkage)",
|
|
12559
|
-
{ operation:
|
|
12768
|
+
{ operation: OP5 }
|
|
12560
12769
|
);
|
|
12561
12770
|
}
|
|
12562
12771
|
if (pins.role !== void 0 && pins.code !== void 0) {
|
|
12563
12772
|
throw new AutotaskConfigError(
|
|
12564
12773
|
"createForResource: a pinned roleID is only meaningful with the task/ticket linkage (internallAllocationCodeID pins the internal linkage and owns the code) \u2014 drop one",
|
|
12565
|
-
{ operation:
|
|
12774
|
+
{ operation: OP5 }
|
|
12566
12775
|
);
|
|
12567
12776
|
}
|
|
12568
12777
|
if (params.hoursWorked !== void 0 && (typeof params.hoursWorked !== "number" || !Number.isFinite(params.hoursWorked) || params.hoursWorked <= 0)) {
|
|
12569
12778
|
throw new AutotaskConfigError(
|
|
12570
12779
|
`createForResource: hoursWorked must be a positive finite number (got ${String(params.hoursWorked)})`,
|
|
12571
|
-
{ operation:
|
|
12780
|
+
{ operation: OP5 }
|
|
12572
12781
|
);
|
|
12573
12782
|
}
|
|
12574
12783
|
return pins;
|
|
12575
12784
|
}
|
|
12576
12785
|
async function assertResourceActive(http, rid) {
|
|
12577
|
-
const rec = await fetchItem2(http, `/V1.0/Resources/${rid}`,
|
|
12786
|
+
const rec = await fetchItem2(http, `/V1.0/Resources/${rid}`, OP5);
|
|
12578
12787
|
if (rec === null) {
|
|
12579
12788
|
throw new ResourceInvalidError(`Resource ${rid} was not found \u2014 a time entry cannot attach to a missing resource`, {
|
|
12580
12789
|
resourceIds: [rid],
|
|
12581
12790
|
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
12582
|
-
operation:
|
|
12791
|
+
operation: OP5,
|
|
12583
12792
|
category: "not_found",
|
|
12584
12793
|
suggestedAction: "Resolve the resource id (resources.get / resources.search) and retry."
|
|
12585
12794
|
});
|
|
@@ -12588,7 +12797,7 @@ async function assertResourceActive(http, rid) {
|
|
|
12588
12797
|
throw new ResourceInvalidError(`Resource ${rid} is not active (isActive !== true) \u2014 the vendor would reject a time entry for an inactive resource`, {
|
|
12589
12798
|
resourceIds: [rid],
|
|
12590
12799
|
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
12591
|
-
operation:
|
|
12800
|
+
operation: OP5,
|
|
12592
12801
|
suggestedAction: "Activate the resource or pick an active one."
|
|
12593
12802
|
});
|
|
12594
12803
|
}
|
|
@@ -12598,11 +12807,11 @@ async function resolveAllocationCode(http, rid, linkage, pins, departments) {
|
|
|
12598
12807
|
const pinnedForCode = pins.code !== void 0 ? pins.code : pins.role;
|
|
12599
12808
|
let code = null;
|
|
12600
12809
|
const familiesTried = [];
|
|
12601
|
-
code = await resolveCode(http, familyOf(linkage), familyLabel(familyOf(linkage)), pinnedForCode, departments,
|
|
12810
|
+
code = await resolveCode(http, familyOf(linkage), familyLabel(familyOf(linkage)), pinnedForCode, departments, OP5);
|
|
12602
12811
|
familiesTried.push({ family: familyOf(linkage), label: familyLabel(familyOf(linkage)), filters: [{ field: "useType", op: "eq", value: familyOf(linkage) }, { field: "isActive", op: "eq", value: true }] });
|
|
12603
12812
|
if (code === null && linkage === "task" && pins.role === void 0) {
|
|
12604
12813
|
usedLinkage = "internal";
|
|
12605
|
-
code = await resolveCode(http, USE_TYPE_INTERNAL, familyLabel(USE_TYPE_INTERNAL), void 0, departments,
|
|
12814
|
+
code = await resolveCode(http, USE_TYPE_INTERNAL, familyLabel(USE_TYPE_INTERNAL), void 0, departments, OP5);
|
|
12606
12815
|
familiesTried.push({ family: USE_TYPE_INTERNAL, label: familyLabel(USE_TYPE_INTERNAL), filters: [{ field: "useType", op: "eq", value: USE_TYPE_INTERNAL }, { field: "isActive", op: "eq", value: true }] });
|
|
12607
12816
|
}
|
|
12608
12817
|
if (code === null) {
|
|
@@ -12612,7 +12821,7 @@ async function resolveAllocationCode(http, rid, linkage, pins, departments) {
|
|
|
12612
12821
|
families: familiesTried,
|
|
12613
12822
|
resourceDepartments: [...departments].sort((a, b) => a - b)
|
|
12614
12823
|
},
|
|
12615
|
-
{ operation:
|
|
12824
|
+
{ operation: OP5 }
|
|
12616
12825
|
);
|
|
12617
12826
|
}
|
|
12618
12827
|
const resolvedCode = codeInfo(code);
|
|
@@ -12651,7 +12860,7 @@ function createDryRun2(http, rid, body, linkage, usedLinkage, resolvedCode) {
|
|
|
12651
12860
|
}
|
|
12652
12861
|
return buildDryRunResult(
|
|
12653
12862
|
{
|
|
12654
|
-
operation:
|
|
12863
|
+
operation: OP5,
|
|
12655
12864
|
method: "POST",
|
|
12656
12865
|
path: "/V1.0/TimeEntries",
|
|
12657
12866
|
ids: [rid],
|
|
@@ -12693,7 +12902,7 @@ async function createWithAttempts(resource, http, opts, body, defaults, pins, us
|
|
|
12693
12902
|
const gate3 = ROLE_REJECTION_PATTERN.test(text);
|
|
12694
12903
|
if (usedLinkage !== "internal" && gate3) {
|
|
12695
12904
|
if (!taskRoleFetched && usedLinkage === "task" && pins.task !== void 0) {
|
|
12696
|
-
const task = await fetchItem2(http, `/V1.0/Tasks/${pins.task}`,
|
|
12905
|
+
const task = await fetchItem2(http, `/V1.0/Tasks/${pins.task}`, OP5);
|
|
12697
12906
|
taskRoleFetched = true;
|
|
12698
12907
|
if (task !== null) pushLadder(task.assignedResourceRoleID);
|
|
12699
12908
|
}
|
|
@@ -12719,7 +12928,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
|
|
|
12719
12928
|
const pins = validateCreateParams(params);
|
|
12720
12929
|
await assertResourceActive(http, rid);
|
|
12721
12930
|
const linkage = pins.code !== void 0 ? "internal" : pins.ticket !== void 0 ? "ticket" : pins.task !== void 0 ? "task" : "internal";
|
|
12722
|
-
const roleRows = await resourceRoleRows(http, rid,
|
|
12931
|
+
const roleRows = await resourceRoleRows(http, rid, OP5);
|
|
12723
12932
|
const departments = roleRows.departments;
|
|
12724
12933
|
const { usedLinkage, resolvedCode } = await resolveAllocationCode(http, rid, linkage, pins, departments);
|
|
12725
12934
|
const defaults = windowDefaults(params);
|
|
@@ -12742,12 +12951,12 @@ function discoveryField(err, body, defaults) {
|
|
|
12742
12951
|
}
|
|
12743
12952
|
|
|
12744
12953
|
// src/resources/special/time_entries/move.ts
|
|
12745
|
-
var
|
|
12954
|
+
var OP6 = "time_entries.moveToTicket";
|
|
12746
12955
|
var COPY2 = ["resourceID", "roleID", "billingCodeID", "dateWorked", "startDateTime", "endDateTime", "hoursWorked", "offsetHours", "summaryNotes", "internalNotes", "isNonBillable", "showOnInvoice"];
|
|
12747
12956
|
var CONTRACT_SCOPED2 = ["contractID", "contractServiceID", "contractServiceBundleID"];
|
|
12748
12957
|
async function readTimeEntry(http, id, retries) {
|
|
12749
12958
|
try {
|
|
12750
|
-
const res = await http.request({ method: "GET", path: `/V1.0/TimeEntries/${id}`, operation:
|
|
12959
|
+
const res = await http.request({ method: "GET", path: `/V1.0/TimeEntries/${id}`, operation: OP6, maxRetries: retries });
|
|
12751
12960
|
return res?.item ?? null;
|
|
12752
12961
|
} catch (e) {
|
|
12753
12962
|
if (isAutotaskError(e) && (e.httpStatus === 404 || e.code === "NOT_FOUND")) return null;
|
|
@@ -12757,27 +12966,27 @@ async function readTimeEntry(http, id, retries) {
|
|
|
12757
12966
|
async function checkSource2({ http, source, sourceId, toTicketId, retries }) {
|
|
12758
12967
|
const ids2 = { sourceId, toTicketId };
|
|
12759
12968
|
if (source.taskID !== null && source.taskID !== void 0) {
|
|
12760
|
-
throw refuse("MOVE_SOURCE_UNSUPPORTED", `${
|
|
12969
|
+
throw refuse("MOVE_SOURCE_UNSUPPORTED", `${OP6}: time entry ${sourceId} is on a task \u2014 only ticket time entries are moved`, ids2, OP6);
|
|
12761
12970
|
}
|
|
12762
12971
|
if (source.billingApprovalDateTime || source.billingApprovalResourceID || source.billingApprovalLevelMostRecent) {
|
|
12763
|
-
throw refuse("MOVE_BLOCKED_APPROVED", `${
|
|
12972
|
+
throw refuse("MOVE_BLOCKED_APPROVED", `${OP6}: time entry ${sourceId} has billing approval set \u2014 move it in the Autotask UI`, ids2, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "approved entries are not moved by the API" } });
|
|
12764
12973
|
}
|
|
12765
12974
|
let billing;
|
|
12766
12975
|
try {
|
|
12767
|
-
billing = await queryRows(http, "BillingItems", [{ field: "timeEntryID", op: "eq", value: sourceId }],
|
|
12976
|
+
billing = await queryRows(http, "BillingItems", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP6, retries);
|
|
12768
12977
|
} catch {
|
|
12769
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
12978
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP6}: the billing items of time entry ${sourceId} could not be read \u2014 refused (fails closed)`, ids2, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billing state unreadable" } });
|
|
12770
12979
|
}
|
|
12771
12980
|
if (billing.some((b) => b.invoiceID || b.postedDate)) {
|
|
12772
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
12981
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP6}: time entry ${sourceId} is billed or posted \u2014 move it in the Autotask UI`, ids2, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billed entries are not moved by the API" } });
|
|
12773
12982
|
}
|
|
12774
|
-
const attachments = await queryRows(http, "TimeEntryAttachments", [{ field: "timeEntryID", op: "eq", value: sourceId }],
|
|
12983
|
+
const attachments = await queryRows(http, "TimeEntryAttachments", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP6, retries);
|
|
12775
12984
|
if (attachments.length > 0) {
|
|
12776
|
-
throw refuse("MOVE_BLOCKED_ATTACHMENTS", `${
|
|
12985
|
+
throw refuse("MOVE_BLOCKED_ATTACHMENTS", `${OP6}: time entry ${sourceId} has attachments, which a delete would destroy \u2014 move it in the Autotask UI`, ids2, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "entries with attachments are not moved by the API" } });
|
|
12777
12986
|
}
|
|
12778
12987
|
}
|
|
12779
12988
|
var SPEC2 = {
|
|
12780
|
-
operation:
|
|
12989
|
+
operation: OP6,
|
|
12781
12990
|
entity: "time_entry",
|
|
12782
12991
|
readSource: readTimeEntry,
|
|
12783
12992
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -12812,7 +13021,7 @@ function moveToTicket2(_resource, sourceId, toTicketId, opts, http) {
|
|
|
12812
13021
|
}
|
|
12813
13022
|
|
|
12814
13023
|
// src/resources/special/time_entries/approve.ts
|
|
12815
|
-
var
|
|
13024
|
+
var OP7 = "time_entries.approve";
|
|
12816
13025
|
var APPROVAL_LEVELS_PATH = "/V1.0/BillingItemApprovalLevels";
|
|
12817
13026
|
function toIso(value) {
|
|
12818
13027
|
if (value instanceof Date) {
|
|
@@ -12850,7 +13059,7 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
12850
13059
|
if (opts.dryRun === true) {
|
|
12851
13060
|
return buildDryRunResult(
|
|
12852
13061
|
{
|
|
12853
|
-
operation:
|
|
13062
|
+
operation: OP7,
|
|
12854
13063
|
method: "POST",
|
|
12855
13064
|
path: APPROVAL_LEVELS_PATH,
|
|
12856
13065
|
ids: [numericId],
|
|
@@ -12873,10 +13082,10 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
12873
13082
|
method: "POST",
|
|
12874
13083
|
path: APPROVAL_LEVELS_PATH,
|
|
12875
13084
|
body,
|
|
12876
|
-
operation:
|
|
13085
|
+
operation: OP7,
|
|
12877
13086
|
maxRetries: opts.maxRetries
|
|
12878
13087
|
});
|
|
12879
|
-
const levelId = toNumber(parseIdentifier(unwrapOperation(created,
|
|
13088
|
+
const levelId = toNumber(parseIdentifier(unwrapOperation(created, OP7).id, "billingItemApprovalLevelID"));
|
|
12880
13089
|
await resource.update(
|
|
12881
13090
|
numericId,
|
|
12882
13091
|
{ billingApprovalDateTime: approvalDateTime, billingApprovalResourceID: approvalResourceID },
|
|
@@ -12899,28 +13108,28 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
12899
13108
|
}
|
|
12900
13109
|
|
|
12901
13110
|
// src/resources/special/time_entries/recentTickets.ts
|
|
12902
|
-
var
|
|
13111
|
+
var OP8 = "time_entries.recentTickets";
|
|
12903
13112
|
var DAY_MS3 = 864e5;
|
|
12904
|
-
var
|
|
12905
|
-
var
|
|
12906
|
-
function
|
|
13113
|
+
var TICKET_CHUNK2 = 100;
|
|
13114
|
+
var TICKET_FIELDS3 = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
13115
|
+
function positiveId2(value, name) {
|
|
12907
13116
|
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
12908
13117
|
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
12909
|
-
throw new AutotaskConfigError(`${
|
|
13118
|
+
throw new AutotaskConfigError(`${OP8}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP8 });
|
|
12910
13119
|
}
|
|
12911
13120
|
return n;
|
|
12912
13121
|
}
|
|
12913
13122
|
async function recentTickets(_resource, from, to, http, opts) {
|
|
12914
13123
|
if (opts === void 0 || opts === null || typeof opts !== "object") {
|
|
12915
|
-
throw new AutotaskConfigError(`${
|
|
13124
|
+
throw new AutotaskConfigError(`${OP8}: opts { resourceID, companyID?, tz?, limit? } is required`, { operation: OP8 });
|
|
12916
13125
|
}
|
|
12917
|
-
const resourceID =
|
|
12918
|
-
const companyID = opts.companyID === void 0 ? void 0 :
|
|
12919
|
-
if (opts.tz !== void 0) validateZone(opts.tz,
|
|
12920
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
12921
|
-
const toB = customRangeDay(to, "to", void 0,
|
|
13126
|
+
const resourceID = positiveId2(opts.resourceID, "resourceID");
|
|
13127
|
+
const companyID = opts.companyID === void 0 ? void 0 : positiveId2(opts.companyID, "companyID");
|
|
13128
|
+
if (opts.tz !== void 0) validateZone(opts.tz, OP8);
|
|
13129
|
+
const fromB = customRangeDay(from, "from", void 0, OP8);
|
|
13130
|
+
const toB = customRangeDay(to, "to", void 0, OP8);
|
|
12922
13131
|
if (fromB.ms > toB.ms) {
|
|
12923
|
-
throw new AutotaskConfigError(`${
|
|
13132
|
+
throw new AutotaskConfigError(`${OP8}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`, { operation: OP8 });
|
|
12924
13133
|
}
|
|
12925
13134
|
const windowToMs = toB.ms + DAY_MS3 - 1;
|
|
12926
13135
|
const { items: entries, hasMore } = await queryTimeEntries(
|
|
@@ -12931,7 +13140,7 @@ async function recentTickets(_resource, from, to, http, opts) {
|
|
|
12931
13140
|
{ field: "resourceID", op: "eq", value: resourceID }
|
|
12932
13141
|
],
|
|
12933
13142
|
clampWindowLimit(opts.limit),
|
|
12934
|
-
|
|
13143
|
+
OP8,
|
|
12935
13144
|
http
|
|
12936
13145
|
);
|
|
12937
13146
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -12949,17 +13158,17 @@ async function recentTickets(_resource, from, to, http, opts) {
|
|
|
12949
13158
|
}
|
|
12950
13159
|
const ids2 = [...groups.keys()];
|
|
12951
13160
|
const tickets = /* @__PURE__ */ new Map();
|
|
12952
|
-
for (let c = 0; c < ids2.length; c +=
|
|
12953
|
-
const chunk = ids2.slice(c, c +
|
|
13161
|
+
for (let c = 0; c < ids2.length; c += TICKET_CHUNK2) {
|
|
13162
|
+
const chunk = ids2.slice(c, c + TICKET_CHUNK2);
|
|
12954
13163
|
const filter = [{ field: "id", op: "in", value: chunk }];
|
|
12955
13164
|
const res = await http.request({
|
|
12956
13165
|
method: "POST",
|
|
12957
13166
|
idempotent: true,
|
|
12958
13167
|
path: "/V1.0/Tickets/query",
|
|
12959
|
-
body: { MaxRecords:
|
|
12960
|
-
operation:
|
|
13168
|
+
body: { MaxRecords: TICKET_CHUNK2, filter, IncludeFields: TICKET_FIELDS3 },
|
|
13169
|
+
operation: OP8
|
|
12961
13170
|
});
|
|
12962
|
-
for (const t of unwrapItems(res,
|
|
13171
|
+
for (const t of unwrapItems(res, OP8).items) if (typeof t.id === "number") tickets.set(t.id, t);
|
|
12963
13172
|
}
|
|
12964
13173
|
const items = [];
|
|
12965
13174
|
const unresolvedTicketIDs = [];
|