node-autotask 0.26.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 +83 -0
- package/MCP_TOOL_CATALOG.json +941 -14
- package/MCP_TOOL_MANIFEST.md +71 -16
- package/N8N_MCP_MANIFEST.json +91 -10
- package/N8N_MCP_MANIFEST.md +14 -11
- package/README.md +11 -9
- package/capabilities.json +13623 -7738
- package/capabilities.schema.json +125 -0
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-QXWIQ67O.js → chunk-6EQSXYRW.js} +13622 -7737
- package/dist/{chunk-HQGE4WAV.cjs → chunk-D5IVS6GY.cjs} +1054 -235
- package/dist/{chunk-5OGJTS2E.cjs → chunk-GUY2KK7U.cjs} +157 -136
- package/dist/{chunk-SSUECUVN.js → chunk-IRVZGW5K.js} +947 -128
- package/dist/{chunk-7DOSJS6S.js → chunk-O2D7KDDD.js} +23 -2
- package/dist/{chunk-VVQ6NJYZ.js → chunk-SYX6EWMT.js} +694 -76
- package/dist/{chunk-SG4LIVMG.cjs → chunk-WYKGD5UD.cjs} +1189 -571
- package/dist/{chunk-WM6E5O55.cjs → chunk-ZEGMVEF3.cjs} +13622 -7737
- package/dist/{index-CKYr0unq.d.cts → index-CORcZUNm.d.cts} +457 -50
- package/dist/{index-1zBdJIEy.d.cts → index-DQ9NgraW.d.cts} +281 -1
- package/dist/{index-1zBdJIEy.d.ts → index-DQ9NgraW.d.ts} +281 -1
- package/dist/{index-C8QH3BkP.d.ts → index-DXuV04zM.d.ts} +457 -50
- package/dist/index.cjs +44 -24
- package/dist/index.d.cts +11 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +26 -6
- package/dist/operations/index.cjs +147 -120
- package/dist/operations/index.d.cts +136 -7
- package/dist/operations/index.d.ts +136 -7
- package/dist/operations/index.js +33 -6
- package/dist/pool.cjs +42 -9
- package/dist/pool.d.cts +14 -3
- package/dist/pool.d.ts +14 -3
- package/dist/pool.js +38 -5
- package/dist/resources/index.cjs +19 -5
- package/dist/resources/index.d.cts +2 -2
- package/dist/resources/index.d.ts +2 -2
- package/dist/resources/index.js +18 -4
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/docs/API.md +246 -10
- package/examples/mcp/schemas.ts +16 -7
- package/package.json +1 -1
- /package/dist/{chunk-S643FDOR.cjs → chunk-IXF7AKOJ.cjs} +0 -0
- /package/dist/{chunk-2TWKEB2L.js → chunk-RMG55JTF.js} +0 -0
- /package/dist/{chunk-W3K5BEVY.cjs → chunk-SDYRQFGL.cjs} +0 -0
- /package/dist/{chunk-EBCHCNPK.js → chunk-WLTLB2ND.js} +0 -0
|
@@ -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 {
|
|
@@ -172,6 +172,42 @@ var BillingCodesResource = class extends BaseResource {
|
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
// src/resources/billing_item_approval_levels.ts
|
|
176
|
+
var BillingItemApprovalLevelsResource = class extends BaseResource {
|
|
177
|
+
constructor(http, pageSize) {
|
|
178
|
+
super(http, {
|
|
179
|
+
key: "billing_item_approval_levels",
|
|
180
|
+
resourcePath: "/V1.0/BillingItemApprovalLevels",
|
|
181
|
+
deletable: false,
|
|
182
|
+
udfCapable: false
|
|
183
|
+
}, pageSize);
|
|
184
|
+
}
|
|
185
|
+
/** Query BillingItemApprovalLevel records with the G3 filter surface (server-side JSON filter). */
|
|
186
|
+
query(params) {
|
|
187
|
+
return this.queryItems(params);
|
|
188
|
+
}
|
|
189
|
+
/** Get every BillingItemApprovalLevel record (the MCP-preferred read; 1000-page guarded). */
|
|
190
|
+
async queryAll(params) {
|
|
191
|
+
return this.queryAllItems(params);
|
|
192
|
+
}
|
|
193
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
194
|
+
async count(params) {
|
|
195
|
+
return this.countGlobal(params);
|
|
196
|
+
}
|
|
197
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
198
|
+
async get(id) {
|
|
199
|
+
return this.getGlobal(id);
|
|
200
|
+
}
|
|
201
|
+
/** Create a record (OperationResultModel → { id}). `dryRun: true` returns the
|
|
202
|
+
* pinned envelope (section 19.3) and never issues the create. A 2xx that is not the
|
|
203
|
+
* specced envelope runs the plan-pinned marker recovery (issue #13 — one bounded read;
|
|
204
|
+
* `recoverIdFromMarker: false` restores the pre-recovery behaviour). */
|
|
205
|
+
async create(data, opts = {}) {
|
|
206
|
+
if (opts.dryRun === true) return this.runCreateDryRun(data);
|
|
207
|
+
return this.createGlobal(data, opts);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
175
211
|
// src/resources/special/dateWindow.ts
|
|
176
212
|
var DAY_MS = 864e5;
|
|
177
213
|
var DEFAULT_LIMIT = 500;
|
|
@@ -1255,6 +1291,82 @@ async function upsertByName(resource, companyName, params, opts, http) {
|
|
|
1255
1291
|
return createWithDiscovery(resource, body, owner, caveat, opts);
|
|
1256
1292
|
}
|
|
1257
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
|
+
|
|
1258
1370
|
// src/resources/special/companies/fullProfile.ts
|
|
1259
1371
|
var FULL_PROFILE_PROJECTABLE_PARTS = ["locations", "contacts", "notes", "projects", "openTickets"];
|
|
1260
1372
|
var PART_ENTITY = {
|
|
@@ -1264,7 +1376,7 @@ var PART_ENTITY = {
|
|
|
1264
1376
|
projects: "projects",
|
|
1265
1377
|
openTickets: "tickets"
|
|
1266
1378
|
};
|
|
1267
|
-
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";
|
|
1268
1380
|
var PROFILE_PART_LIMIT = 50;
|
|
1269
1381
|
var LOCATION_LIMIT_MAX = 500;
|
|
1270
1382
|
function locationLimit(value) {
|
|
@@ -1325,6 +1437,8 @@ async function scopedCompanyQuery(http, path, companyID, limit, includeFields) {
|
|
|
1325
1437
|
return project(unwrapItems(res).items, includeFields);
|
|
1326
1438
|
}
|
|
1327
1439
|
async function readOpenTickets(http, companyID, ticketLimit, includeFields) {
|
|
1440
|
+
const semantics = await resolveTicketStatusSemantics(http);
|
|
1441
|
+
const openLeaf = openStatusFilter(semantics.closedIds);
|
|
1328
1442
|
const openRows = await (async () => {
|
|
1329
1443
|
const res = await http.request({
|
|
1330
1444
|
method: "POST",
|
|
@@ -1332,30 +1446,31 @@ async function readOpenTickets(http, companyID, ticketLimit, includeFields) {
|
|
|
1332
1446
|
path: "/V1.0/Tickets/query",
|
|
1333
1447
|
body: {
|
|
1334
1448
|
MaxRecords: ticketLimit,
|
|
1335
|
-
filter: [{ field: "companyID", op: "eq", value: companyID },
|
|
1449
|
+
filter: [{ field: "companyID", op: "eq", value: companyID }, openLeaf],
|
|
1336
1450
|
...includeFields ? { IncludeFields: includeFields } : {}
|
|
1337
1451
|
},
|
|
1338
1452
|
operation: "companies.fullProfile"
|
|
1339
1453
|
});
|
|
1340
1454
|
return project(unwrapItems(res).items, includeFields);
|
|
1341
1455
|
})();
|
|
1456
|
+
const closedStatus = { closedIds: semantics.closedIds, source: semantics.source, note: semantics.note };
|
|
1457
|
+
const partNote = semantics.source === "assumed" ? OPEN_TICKETS_NOTE : semantics.note;
|
|
1342
1458
|
let openTickets = {
|
|
1343
1459
|
items: openRows,
|
|
1344
1460
|
capped: openRows.length >= ticketLimit,
|
|
1345
1461
|
blind: false,
|
|
1346
|
-
note:
|
|
1462
|
+
note: partNote,
|
|
1463
|
+
closedStatus
|
|
1347
1464
|
};
|
|
1348
1465
|
if (openRows.length === 0) {
|
|
1349
|
-
const stamp = blindStampFor(BLIND_SURFACE_STAMPS["tickets"]?.query, [
|
|
1350
|
-
{ field: "companyID", op: "eq", value: companyID },
|
|
1351
|
-
{ field: "status", op: "noteq", value: 5 }
|
|
1352
|
-
]);
|
|
1466
|
+
const stamp = blindStampFor(BLIND_SURFACE_STAMPS["tickets"]?.query, [{ field: "companyID", op: "eq", value: companyID }, openLeaf]);
|
|
1353
1467
|
if (stamp !== void 0) {
|
|
1354
1468
|
openTickets = {
|
|
1355
1469
|
items: openRows,
|
|
1356
1470
|
capped: openRows.length >= ticketLimit,
|
|
1357
1471
|
blind: true,
|
|
1358
|
-
note: `${stamp.note} ${
|
|
1472
|
+
note: `${stamp.note} ${partNote}`,
|
|
1473
|
+
closedStatus,
|
|
1359
1474
|
blindSurface: stamp
|
|
1360
1475
|
};
|
|
1361
1476
|
}
|
|
@@ -1553,7 +1668,7 @@ function validateUnbilledWorkOptions(companyId, opts) {
|
|
|
1553
1668
|
}
|
|
1554
1669
|
|
|
1555
1670
|
// src/resources/special/companies/unbilledWork.ts
|
|
1556
|
-
var
|
|
1671
|
+
var OP2 = "companies.unbilledWork";
|
|
1557
1672
|
function capJoinSet(ids2, cap) {
|
|
1558
1673
|
const sorted = [...new Set(ids2)].sort((a, b) => a - b);
|
|
1559
1674
|
if (sorted.length <= cap) return { ids: sorted, truncated: false };
|
|
@@ -1590,14 +1705,14 @@ async function readCompanyAndParent(resource, id, includeParent) {
|
|
|
1590
1705
|
}
|
|
1591
1706
|
async function readJoinSets(http, companyIds, v) {
|
|
1592
1707
|
const ticketsCond = [{ field: "companyID", op: "in", value: companyIds }];
|
|
1593
|
-
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);
|
|
1594
1709
|
const projectsCond = [{ field: "companyID", op: "in", value: companyIds }];
|
|
1595
|
-
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);
|
|
1596
1711
|
const projectIds = projectsRaw.items.map((p) => num(p.id)).filter((n) => n > 0);
|
|
1597
1712
|
let taskIds = [];
|
|
1598
1713
|
let tasksHasMore = false;
|
|
1599
1714
|
if (projectIds.length > 0) {
|
|
1600
|
-
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);
|
|
1601
1716
|
taskIds = tasksRaw.items.map((t) => num(t.id)).filter((n) => n > 0);
|
|
1602
1717
|
tasksHasMore = tasksRaw.hasMore;
|
|
1603
1718
|
}
|
|
@@ -1621,7 +1736,7 @@ async function readBillingPart(http, v, companyIds) {
|
|
|
1621
1736
|
}
|
|
1622
1737
|
const billingFields = resolveFieldsPreset("billing_join", "billing_items");
|
|
1623
1738
|
const billingIncludeFields = [...billingFields, "id", ...v.includeNotes ? ["description"] : []];
|
|
1624
|
-
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);
|
|
1625
1740
|
let billingItems = billingRaw.items;
|
|
1626
1741
|
let billingNoteCharsTruncated = 0;
|
|
1627
1742
|
if (v.includeNotes) {
|
|
@@ -1665,7 +1780,7 @@ async function readTimePart(http, v, join) {
|
|
|
1665
1780
|
if (v.dateFrom !== void 0) timeConditions.push({ field: "dateWorked", op: "gte", value: v.dateFrom });
|
|
1666
1781
|
if (v.dateTo !== void 0) timeConditions.push({ field: "dateWorked", op: "lte", value: v.dateTo });
|
|
1667
1782
|
const timeFields = ["taskID", "ticketID", "hoursWorked", "dateWorked", "id", ...v.includeNotes ? ["summaryNotes"] : []];
|
|
1668
|
-
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);
|
|
1669
1784
|
entries = raw.items;
|
|
1670
1785
|
let cappedItems = entries;
|
|
1671
1786
|
let noteCharsTruncated = 0;
|
|
@@ -5184,7 +5299,7 @@ function appendWiresNote(wires, note) {
|
|
|
5184
5299
|
wires.note = (wires.note ? wires.note + " | " : "") + note;
|
|
5185
5300
|
}
|
|
5186
5301
|
function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
5187
|
-
const
|
|
5302
|
+
const OP9 = PROFITABILITY_OP;
|
|
5188
5303
|
const id = parseIdentifier(contractId, "contractId");
|
|
5189
5304
|
const idNum = typeof id === "number" ? id : Number(id);
|
|
5190
5305
|
const wires = { issued: 0, requests: 0, budget };
|
|
@@ -5196,19 +5311,19 @@ function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
|
5196
5311
|
} };
|
|
5197
5312
|
const readPart = (path, conditions, fields) => {
|
|
5198
5313
|
wires.issued += 1;
|
|
5199
|
-
return degradable(() => boundedPart(http, path, conditions, v.limit,
|
|
5314
|
+
return degradable(() => boundedPart(http, path, conditions, v.limit, OP9, fields));
|
|
5200
5315
|
};
|
|
5201
5316
|
const truncatedMonths = {};
|
|
5202
5317
|
const windowedPart = async (name, key, path, cond, fields, dateField) => {
|
|
5203
5318
|
const first = await readPart(path, cond, fields);
|
|
5204
5319
|
if (first.absent !== void 0 || !first.hasMore) return { part: stampPart(key, first, cond), rows: first.items };
|
|
5205
|
-
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));
|
|
5206
5321
|
if (!("truncatedMonths" in wp)) return { part: stampPart(key, wp, cond), rows: [] };
|
|
5207
5322
|
const { truncatedMonths: tm, ...part } = wp;
|
|
5208
5323
|
truncatedMonths[name] = tm;
|
|
5209
5324
|
return { part, rows: wp.items };
|
|
5210
5325
|
};
|
|
5211
|
-
return { v, OP:
|
|
5326
|
+
return { v, OP: OP9, id, idNum, rawHttp, http, wires, windowOn, readPart, windowedPart, truncatedMonths };
|
|
5212
5327
|
}
|
|
5213
5328
|
|
|
5214
5329
|
// src/resources/special/contracts/profitability.stages.ts
|
|
@@ -7131,6 +7246,34 @@ var InvoicesResource = class extends BaseResource {
|
|
|
7131
7246
|
}
|
|
7132
7247
|
};
|
|
7133
7248
|
|
|
7249
|
+
// src/resources/invoice_templates.ts
|
|
7250
|
+
var InvoiceTemplatesResource = class extends BaseResource {
|
|
7251
|
+
constructor(http, pageSize) {
|
|
7252
|
+
super(http, {
|
|
7253
|
+
key: "invoice_templates",
|
|
7254
|
+
resourcePath: "/V1.0/InvoiceTemplates",
|
|
7255
|
+
deletable: false,
|
|
7256
|
+
udfCapable: false
|
|
7257
|
+
}, pageSize);
|
|
7258
|
+
}
|
|
7259
|
+
/** Query InvoiceTemplate records with the G3 filter surface (server-side JSON filter). */
|
|
7260
|
+
query(params) {
|
|
7261
|
+
return this.queryItems(params);
|
|
7262
|
+
}
|
|
7263
|
+
/** Get every InvoiceTemplate record (the MCP-preferred read; 1000-page guarded). */
|
|
7264
|
+
async queryAll(params) {
|
|
7265
|
+
return this.queryAllItems(params);
|
|
7266
|
+
}
|
|
7267
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
7268
|
+
async count(params) {
|
|
7269
|
+
return this.countGlobal(params);
|
|
7270
|
+
}
|
|
7271
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
7272
|
+
async get(id) {
|
|
7273
|
+
return this.getGlobal(id);
|
|
7274
|
+
}
|
|
7275
|
+
};
|
|
7276
|
+
|
|
7134
7277
|
// src/resources/modules.ts
|
|
7135
7278
|
var ModulesResource = class {
|
|
7136
7279
|
http;
|
|
@@ -7369,6 +7512,159 @@ var OpportunityCategoriesResource = class extends BaseResource {
|
|
|
7369
7512
|
}
|
|
7370
7513
|
};
|
|
7371
7514
|
|
|
7515
|
+
// src/resources/organizational_level1s.ts
|
|
7516
|
+
var OrganizationalLevel1sResource = class extends BaseResource {
|
|
7517
|
+
constructor(http, pageSize) {
|
|
7518
|
+
super(http, {
|
|
7519
|
+
key: "organizational_level1s",
|
|
7520
|
+
resourcePath: "/V1.0/OrganizationalLevel1s",
|
|
7521
|
+
deletable: false,
|
|
7522
|
+
udfCapable: false,
|
|
7523
|
+
collectionUpdate: true
|
|
7524
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
7525
|
+
}, pageSize);
|
|
7526
|
+
}
|
|
7527
|
+
/** Query OrganizationalLevel1 records with the G3 filter surface (server-side JSON filter). */
|
|
7528
|
+
query(params) {
|
|
7529
|
+
return this.queryItems(params);
|
|
7530
|
+
}
|
|
7531
|
+
/** Get every OrganizationalLevel1 record (the MCP-preferred read; 1000-page guarded). */
|
|
7532
|
+
async queryAll(params) {
|
|
7533
|
+
return this.queryAllItems(params);
|
|
7534
|
+
}
|
|
7535
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
7536
|
+
async count(params) {
|
|
7537
|
+
return this.countGlobal(params);
|
|
7538
|
+
}
|
|
7539
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
7540
|
+
async get(id) {
|
|
7541
|
+
return this.getGlobal(id);
|
|
7542
|
+
}
|
|
7543
|
+
/** Create a record (OperationResultModel → { id}). `dryRun: true` returns the
|
|
7544
|
+
* pinned envelope (section 19.3) and never issues the create. A 2xx that is not the
|
|
7545
|
+
* specced envelope runs the plan-pinned marker recovery (issue #13 — one bounded read;
|
|
7546
|
+
* `recoverIdFromMarker: false` restores the pre-recovery behaviour). */
|
|
7547
|
+
async create(data, opts = {}) {
|
|
7548
|
+
if (opts.dryRun === true) return this.runCreateDryRun(data);
|
|
7549
|
+
return this.createGlobal(data, opts);
|
|
7550
|
+
}
|
|
7551
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
7552
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
7553
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
7554
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
7555
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
7556
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
7557
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
7558
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
7559
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
7560
|
+
async update(id, data, opts = {}) {
|
|
7561
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
7562
|
+
return this.updateGlobal(id, data, opts);
|
|
7563
|
+
}
|
|
7564
|
+
};
|
|
7565
|
+
|
|
7566
|
+
// src/resources/organizational_level2s.ts
|
|
7567
|
+
var OrganizationalLevel2sResource = class extends BaseResource {
|
|
7568
|
+
constructor(http, pageSize) {
|
|
7569
|
+
super(http, {
|
|
7570
|
+
key: "organizational_level2s",
|
|
7571
|
+
resourcePath: "/V1.0/OrganizationalLevel2s",
|
|
7572
|
+
deletable: false,
|
|
7573
|
+
udfCapable: false,
|
|
7574
|
+
collectionUpdate: true
|
|
7575
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
7576
|
+
}, pageSize);
|
|
7577
|
+
}
|
|
7578
|
+
/** Query OrganizationalLevel2 records with the G3 filter surface (server-side JSON filter). */
|
|
7579
|
+
query(params) {
|
|
7580
|
+
return this.queryItems(params);
|
|
7581
|
+
}
|
|
7582
|
+
/** Get every OrganizationalLevel2 record (the MCP-preferred read; 1000-page guarded). */
|
|
7583
|
+
async queryAll(params) {
|
|
7584
|
+
return this.queryAllItems(params);
|
|
7585
|
+
}
|
|
7586
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
7587
|
+
async count(params) {
|
|
7588
|
+
return this.countGlobal(params);
|
|
7589
|
+
}
|
|
7590
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
7591
|
+
async get(id) {
|
|
7592
|
+
return this.getGlobal(id);
|
|
7593
|
+
}
|
|
7594
|
+
/** Create a record (OperationResultModel → { id}). `dryRun: true` returns the
|
|
7595
|
+
* pinned envelope (section 19.3) and never issues the create. A 2xx that is not the
|
|
7596
|
+
* specced envelope runs the plan-pinned marker recovery (issue #13 — one bounded read;
|
|
7597
|
+
* `recoverIdFromMarker: false` restores the pre-recovery behaviour). */
|
|
7598
|
+
async create(data, opts = {}) {
|
|
7599
|
+
if (opts.dryRun === true) return this.runCreateDryRun(data);
|
|
7600
|
+
return this.createGlobal(data, opts);
|
|
7601
|
+
}
|
|
7602
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
7603
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
7604
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
7605
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
7606
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
7607
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
7608
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
7609
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
7610
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
7611
|
+
async update(id, data, opts = {}) {
|
|
7612
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
7613
|
+
return this.updateGlobal(id, data, opts);
|
|
7614
|
+
}
|
|
7615
|
+
};
|
|
7616
|
+
|
|
7617
|
+
// src/resources/organizational_level_associations.ts
|
|
7618
|
+
var OrganizationalLevelAssociationsResource = class extends BaseResource {
|
|
7619
|
+
constructor(http, pageSize) {
|
|
7620
|
+
super(http, {
|
|
7621
|
+
key: "organizational_level_associations",
|
|
7622
|
+
resourcePath: "/V1.0/OrganizationalLevelAssociations",
|
|
7623
|
+
deletable: false,
|
|
7624
|
+
udfCapable: false,
|
|
7625
|
+
collectionUpdate: true
|
|
7626
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
7627
|
+
}, pageSize);
|
|
7628
|
+
}
|
|
7629
|
+
/** Query OrganizationalLevelAssociation records with the G3 filter surface (server-side JSON filter). */
|
|
7630
|
+
query(params) {
|
|
7631
|
+
return this.queryItems(params);
|
|
7632
|
+
}
|
|
7633
|
+
/** Get every OrganizationalLevelAssociation record (the MCP-preferred read; 1000-page guarded). */
|
|
7634
|
+
async queryAll(params) {
|
|
7635
|
+
return this.queryAllItems(params);
|
|
7636
|
+
}
|
|
7637
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
7638
|
+
async count(params) {
|
|
7639
|
+
return this.countGlobal(params);
|
|
7640
|
+
}
|
|
7641
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
7642
|
+
async get(id) {
|
|
7643
|
+
return this.getGlobal(id);
|
|
7644
|
+
}
|
|
7645
|
+
/** Create a record (OperationResultModel → { id}). `dryRun: true` returns the
|
|
7646
|
+
* pinned envelope (section 19.3) and never issues the create. A 2xx that is not the
|
|
7647
|
+
* specced envelope runs the plan-pinned marker recovery (issue #13 — one bounded read;
|
|
7648
|
+
* `recoverIdFromMarker: false` restores the pre-recovery behaviour). */
|
|
7649
|
+
async create(data, opts = {}) {
|
|
7650
|
+
if (opts.dryRun === true) return this.runCreateDryRun(data);
|
|
7651
|
+
return this.createGlobal(data, opts);
|
|
7652
|
+
}
|
|
7653
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
7654
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
7655
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
7656
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
7657
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
7658
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
7659
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
7660
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
7661
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
7662
|
+
async update(id, data, opts = {}) {
|
|
7663
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
7664
|
+
return this.updateGlobal(id, data, opts);
|
|
7665
|
+
}
|
|
7666
|
+
};
|
|
7667
|
+
|
|
7372
7668
|
// src/resources/phases.ts
|
|
7373
7669
|
var PhasesResource = class extends BaseResource {
|
|
7374
7670
|
constructor(http, pageSize) {
|
|
@@ -8807,6 +9103,49 @@ var RolesResource = class extends BaseResource {
|
|
|
8807
9103
|
}
|
|
8808
9104
|
};
|
|
8809
9105
|
|
|
9106
|
+
// src/resources/sales_orders.ts
|
|
9107
|
+
var SalesOrdersResource = class extends BaseResource {
|
|
9108
|
+
constructor(http, pageSize) {
|
|
9109
|
+
super(http, {
|
|
9110
|
+
key: "sales_orders",
|
|
9111
|
+
resourcePath: "/V1.0/SalesOrders",
|
|
9112
|
+
deletable: false,
|
|
9113
|
+
udfCapable: true,
|
|
9114
|
+
collectionUpdate: true
|
|
9115
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
9116
|
+
}, pageSize);
|
|
9117
|
+
}
|
|
9118
|
+
/** Query SalesOrder records with the G3 filter surface (server-side JSON filter). */
|
|
9119
|
+
query(params) {
|
|
9120
|
+
return this.queryItems(params);
|
|
9121
|
+
}
|
|
9122
|
+
/** Get every SalesOrder record (the MCP-preferred read; 1000-page guarded). */
|
|
9123
|
+
async queryAll(params) {
|
|
9124
|
+
return this.queryAllItems(params);
|
|
9125
|
+
}
|
|
9126
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
9127
|
+
async count(params) {
|
|
9128
|
+
return this.countGlobal(params);
|
|
9129
|
+
}
|
|
9130
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
9131
|
+
async get(id) {
|
|
9132
|
+
return this.getGlobal(id);
|
|
9133
|
+
}
|
|
9134
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
9135
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
9136
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
9137
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
9138
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
9139
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
9140
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
9141
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
9142
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
9143
|
+
async update(id, data, opts = {}) {
|
|
9144
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
9145
|
+
return this.updateGlobal(id, data, opts);
|
|
9146
|
+
}
|
|
9147
|
+
};
|
|
9148
|
+
|
|
8810
9149
|
// src/resources/service_calls.ts
|
|
8811
9150
|
var ServiceCallsResource = class extends BaseResource {
|
|
8812
9151
|
constructor(http, pageSize) {
|
|
@@ -9762,6 +10101,57 @@ var TasksResource = class extends BaseResource {
|
|
|
9762
10101
|
}
|
|
9763
10102
|
};
|
|
9764
10103
|
|
|
10104
|
+
// src/resources/tax_regions.ts
|
|
10105
|
+
var TaxRegionsResource = class extends BaseResource {
|
|
10106
|
+
constructor(http, pageSize) {
|
|
10107
|
+
super(http, {
|
|
10108
|
+
key: "tax_regions",
|
|
10109
|
+
resourcePath: "/V1.0/TaxRegions",
|
|
10110
|
+
deletable: false,
|
|
10111
|
+
udfCapable: false,
|
|
10112
|
+
collectionUpdate: true
|
|
10113
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
10114
|
+
}, pageSize);
|
|
10115
|
+
}
|
|
10116
|
+
/** Query TaxRegion records with the G3 filter surface (server-side JSON filter). */
|
|
10117
|
+
query(params) {
|
|
10118
|
+
return this.queryItems(params);
|
|
10119
|
+
}
|
|
10120
|
+
/** Get every TaxRegion record (the MCP-preferred read; 1000-page guarded). */
|
|
10121
|
+
async queryAll(params) {
|
|
10122
|
+
return this.queryAllItems(params);
|
|
10123
|
+
}
|
|
10124
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
10125
|
+
async count(params) {
|
|
10126
|
+
return this.countGlobal(params);
|
|
10127
|
+
}
|
|
10128
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). */
|
|
10129
|
+
async get(id) {
|
|
10130
|
+
return this.getGlobal(id);
|
|
10131
|
+
}
|
|
10132
|
+
/** Create a record (OperationResultModel → { id}). `dryRun: true` returns the
|
|
10133
|
+
* pinned envelope (section 19.3) and never issues the create. A 2xx that is not the
|
|
10134
|
+
* specced envelope runs the plan-pinned marker recovery (issue #13 — one bounded read;
|
|
10135
|
+
* `recoverIdFromMarker: false` restores the pre-recovery behaviour). */
|
|
10136
|
+
async create(data, opts = {}) {
|
|
10137
|
+
if (opts.dryRun === true) return this.runCreateDryRun(data);
|
|
10138
|
+
return this.createGlobal(data, opts);
|
|
10139
|
+
}
|
|
10140
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
10141
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
10142
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
10143
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
10144
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
10145
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
10146
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
10147
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
10148
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
10149
|
+
async update(id, data, opts = {}) {
|
|
10150
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
10151
|
+
return this.updateGlobal(id, data, opts);
|
|
10152
|
+
}
|
|
10153
|
+
};
|
|
10154
|
+
|
|
9765
10155
|
// src/resources/threshold_information.ts
|
|
9766
10156
|
var ThresholdInformationResource = class {
|
|
9767
10157
|
http;
|
|
@@ -10123,7 +10513,6 @@ var TicketChangeRequestApprovalsResource = class extends BaseResource {
|
|
|
10123
10513
|
// src/resources/special/_shared/move-engine.ts
|
|
10124
10514
|
import { randomUUID } from "crypto";
|
|
10125
10515
|
var MOVE_SOURCE_AGE_DEFAULT_DAYS = 14;
|
|
10126
|
-
var TICKET_COMPLETE_STATUS = 5;
|
|
10127
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";
|
|
10128
10517
|
var refuse = (code, message, ids2, operation, extra = {}) => new MoveRefusedError(message, { code, operation, details: { status: "failed", ...ids2, ...extra } });
|
|
10129
10518
|
async function queryRows(http, resource, filter, operation, retries) {
|
|
@@ -10176,11 +10565,19 @@ async function preview(http, spec, sourceId, toTicketId, opts) {
|
|
|
10176
10565
|
}
|
|
10177
10566
|
const target = await readTicket(http, toTicketId, opts.maxRetries, op);
|
|
10178
10567
|
if (target === null) throw refuse("MOVE_TARGET_MISSING", `${op}: target ticket ${toTicketId} does not exist`, ids2, op);
|
|
10179
|
-
|
|
10180
|
-
|
|
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
|
+
);
|
|
10181
10577
|
}
|
|
10182
10578
|
const origin = await readTicket(http, sourceTicketId, opts.maxRetries, op);
|
|
10183
10579
|
const facts = {
|
|
10580
|
+
closedStatus: { closedIds: closedStatus.closedIds, source: closedStatus.source, note: closedStatus.note },
|
|
10184
10581
|
sourceTicketId,
|
|
10185
10582
|
sourceCompanyID: origin?.companyID ?? null,
|
|
10186
10583
|
targetCompanyID: target.companyID ?? null,
|
|
@@ -10294,22 +10691,22 @@ function uiLinkFor(http, entity, id) {
|
|
|
10294
10691
|
}
|
|
10295
10692
|
|
|
10296
10693
|
// src/resources/special/ticket_charges.ts
|
|
10297
|
-
var
|
|
10694
|
+
var OP3 = "ticket_charges.moveToTicket";
|
|
10298
10695
|
var COPY = ["name", "description", "chargeType", "billingCodeID", "productID", "unitQuantity", "unitCost", "unitPrice", "datePurchased", "isBillableToCompany", "notes", "purchaseOrderNumber", "internalPurchaseOrderNumber"];
|
|
10299
10696
|
var CONTRACT_SCOPED = ["contractServiceID", "contractServiceBundleID"];
|
|
10300
10697
|
async function readCharge(http, id, retries) {
|
|
10301
|
-
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);
|
|
10302
10699
|
return rows[0] ?? null;
|
|
10303
10700
|
}
|
|
10304
10701
|
async function checkSource({ http, source, sourceId, sourceTicketId, toTicketId }) {
|
|
10305
10702
|
if (source.isBilled === true) {
|
|
10306
|
-
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, {
|
|
10307
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)" }
|
|
10308
10705
|
});
|
|
10309
10706
|
}
|
|
10310
10707
|
}
|
|
10311
10708
|
var SPEC = {
|
|
10312
|
-
operation:
|
|
10709
|
+
operation: OP3,
|
|
10313
10710
|
entity: "ticket_charge",
|
|
10314
10711
|
readSource: readCharge,
|
|
10315
10712
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -11750,6 +12147,121 @@ async function fullContext(resource, id, opts = {}, http) {
|
|
|
11750
12147
|
return result;
|
|
11751
12148
|
}
|
|
11752
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
|
+
|
|
11753
12265
|
// src/resources/tickets.ts
|
|
11754
12266
|
var TicketsResource = class extends BaseResource {
|
|
11755
12267
|
constructor(http, pageSize) {
|
|
@@ -11855,6 +12367,14 @@ var TicketsResource = class extends BaseResource {
|
|
|
11855
12367
|
fullContext(id, opts) {
|
|
11856
12368
|
return fullContext(this, id, opts, this.http);
|
|
11857
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
|
+
}
|
|
11858
12378
|
};
|
|
11859
12379
|
|
|
11860
12380
|
// src/resources/ticket_webhooks.ts
|
|
@@ -11983,12 +12503,12 @@ async function postedDateStatusQuery(_resource, mode, http, opts) {
|
|
|
11983
12503
|
return items;
|
|
11984
12504
|
}
|
|
11985
12505
|
async function postedWithin(_resource, from, to, http, opts, opLabel = "time_entries.postedWithin") {
|
|
11986
|
-
const
|
|
11987
|
-
if (opts?.tz !== void 0) validateZone(opts.tz,
|
|
11988
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
11989
|
-
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);
|
|
11990
12510
|
if (fromB.ms > toB.ms) {
|
|
11991
|
-
throw new AutotaskConfigError(`${
|
|
12511
|
+
throw new AutotaskConfigError(`${OP9}: from (${iso2(fromB.ms)}) must not be after to (${iso2(toB.ms)})`, { operation: OP9 });
|
|
11992
12512
|
}
|
|
11993
12513
|
const windowToMs = toB.ms + DAY_MS2 - 1;
|
|
11994
12514
|
const filter = [
|
|
@@ -11999,7 +12519,7 @@ async function postedWithin(_resource, from, to, http, opts, opLabel = "time_ent
|
|
|
11999
12519
|
else if (opts?.posted === false) filter.push({ field: "postedDate", op: "notExist" });
|
|
12000
12520
|
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
12001
12521
|
if (opts?.resourceID !== void 0) filter.push({ field: "resourceID", op: "eq", value: opts.resourceID });
|
|
12002
|
-
const { items, hasMore } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit),
|
|
12522
|
+
const { items, hasMore } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), OP9, http);
|
|
12003
12523
|
let postedCount = 0;
|
|
12004
12524
|
for (const item of items) {
|
|
12005
12525
|
const postedDate = item.postedDate;
|
|
@@ -12209,7 +12729,7 @@ async function resolveCode(http, family, familyLabel2, pinnedId, departments, op
|
|
|
12209
12729
|
}
|
|
12210
12730
|
|
|
12211
12731
|
// src/resources/special/time_entries/createForResource.ts
|
|
12212
|
-
var
|
|
12732
|
+
var OP5 = "time_entries.createForResource";
|
|
12213
12733
|
var PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
12214
12734
|
"internallAllocationCodeID",
|
|
12215
12735
|
"ticketID",
|
|
@@ -12245,30 +12765,30 @@ function validateCreateParams(params) {
|
|
|
12245
12765
|
if (pinCount > 1) {
|
|
12246
12766
|
throw new AutotaskConfigError(
|
|
12247
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)",
|
|
12248
|
-
{ operation:
|
|
12768
|
+
{ operation: OP5 }
|
|
12249
12769
|
);
|
|
12250
12770
|
}
|
|
12251
12771
|
if (pins.role !== void 0 && pins.code !== void 0) {
|
|
12252
12772
|
throw new AutotaskConfigError(
|
|
12253
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",
|
|
12254
|
-
{ operation:
|
|
12774
|
+
{ operation: OP5 }
|
|
12255
12775
|
);
|
|
12256
12776
|
}
|
|
12257
12777
|
if (params.hoursWorked !== void 0 && (typeof params.hoursWorked !== "number" || !Number.isFinite(params.hoursWorked) || params.hoursWorked <= 0)) {
|
|
12258
12778
|
throw new AutotaskConfigError(
|
|
12259
12779
|
`createForResource: hoursWorked must be a positive finite number (got ${String(params.hoursWorked)})`,
|
|
12260
|
-
{ operation:
|
|
12780
|
+
{ operation: OP5 }
|
|
12261
12781
|
);
|
|
12262
12782
|
}
|
|
12263
12783
|
return pins;
|
|
12264
12784
|
}
|
|
12265
12785
|
async function assertResourceActive(http, rid) {
|
|
12266
|
-
const rec = await fetchItem2(http, `/V1.0/Resources/${rid}`,
|
|
12786
|
+
const rec = await fetchItem2(http, `/V1.0/Resources/${rid}`, OP5);
|
|
12267
12787
|
if (rec === null) {
|
|
12268
12788
|
throw new ResourceInvalidError(`Resource ${rid} was not found \u2014 a time entry cannot attach to a missing resource`, {
|
|
12269
12789
|
resourceIds: [rid],
|
|
12270
12790
|
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
12271
|
-
operation:
|
|
12791
|
+
operation: OP5,
|
|
12272
12792
|
category: "not_found",
|
|
12273
12793
|
suggestedAction: "Resolve the resource id (resources.get / resources.search) and retry."
|
|
12274
12794
|
});
|
|
@@ -12277,7 +12797,7 @@ async function assertResourceActive(http, rid) {
|
|
|
12277
12797
|
throw new ResourceInvalidError(`Resource ${rid} is not active (isActive !== true) \u2014 the vendor would reject a time entry for an inactive resource`, {
|
|
12278
12798
|
resourceIds: [rid],
|
|
12279
12799
|
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
12280
|
-
operation:
|
|
12800
|
+
operation: OP5,
|
|
12281
12801
|
suggestedAction: "Activate the resource or pick an active one."
|
|
12282
12802
|
});
|
|
12283
12803
|
}
|
|
@@ -12287,11 +12807,11 @@ async function resolveAllocationCode(http, rid, linkage, pins, departments) {
|
|
|
12287
12807
|
const pinnedForCode = pins.code !== void 0 ? pins.code : pins.role;
|
|
12288
12808
|
let code = null;
|
|
12289
12809
|
const familiesTried = [];
|
|
12290
|
-
code = await resolveCode(http, familyOf(linkage), familyLabel(familyOf(linkage)), pinnedForCode, departments,
|
|
12810
|
+
code = await resolveCode(http, familyOf(linkage), familyLabel(familyOf(linkage)), pinnedForCode, departments, OP5);
|
|
12291
12811
|
familiesTried.push({ family: familyOf(linkage), label: familyLabel(familyOf(linkage)), filters: [{ field: "useType", op: "eq", value: familyOf(linkage) }, { field: "isActive", op: "eq", value: true }] });
|
|
12292
12812
|
if (code === null && linkage === "task" && pins.role === void 0) {
|
|
12293
12813
|
usedLinkage = "internal";
|
|
12294
|
-
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);
|
|
12295
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 }] });
|
|
12296
12816
|
}
|
|
12297
12817
|
if (code === null) {
|
|
@@ -12301,7 +12821,7 @@ async function resolveAllocationCode(http, rid, linkage, pins, departments) {
|
|
|
12301
12821
|
families: familiesTried,
|
|
12302
12822
|
resourceDepartments: [...departments].sort((a, b) => a - b)
|
|
12303
12823
|
},
|
|
12304
|
-
{ operation:
|
|
12824
|
+
{ operation: OP5 }
|
|
12305
12825
|
);
|
|
12306
12826
|
}
|
|
12307
12827
|
const resolvedCode = codeInfo(code);
|
|
@@ -12340,7 +12860,7 @@ function createDryRun2(http, rid, body, linkage, usedLinkage, resolvedCode) {
|
|
|
12340
12860
|
}
|
|
12341
12861
|
return buildDryRunResult(
|
|
12342
12862
|
{
|
|
12343
|
-
operation:
|
|
12863
|
+
operation: OP5,
|
|
12344
12864
|
method: "POST",
|
|
12345
12865
|
path: "/V1.0/TimeEntries",
|
|
12346
12866
|
ids: [rid],
|
|
@@ -12382,7 +12902,7 @@ async function createWithAttempts(resource, http, opts, body, defaults, pins, us
|
|
|
12382
12902
|
const gate3 = ROLE_REJECTION_PATTERN.test(text);
|
|
12383
12903
|
if (usedLinkage !== "internal" && gate3) {
|
|
12384
12904
|
if (!taskRoleFetched && usedLinkage === "task" && pins.task !== void 0) {
|
|
12385
|
-
const task = await fetchItem2(http, `/V1.0/Tasks/${pins.task}`,
|
|
12905
|
+
const task = await fetchItem2(http, `/V1.0/Tasks/${pins.task}`, OP5);
|
|
12386
12906
|
taskRoleFetched = true;
|
|
12387
12907
|
if (task !== null) pushLadder(task.assignedResourceRoleID);
|
|
12388
12908
|
}
|
|
@@ -12408,7 +12928,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
|
|
|
12408
12928
|
const pins = validateCreateParams(params);
|
|
12409
12929
|
await assertResourceActive(http, rid);
|
|
12410
12930
|
const linkage = pins.code !== void 0 ? "internal" : pins.ticket !== void 0 ? "ticket" : pins.task !== void 0 ? "task" : "internal";
|
|
12411
|
-
const roleRows = await resourceRoleRows(http, rid,
|
|
12931
|
+
const roleRows = await resourceRoleRows(http, rid, OP5);
|
|
12412
12932
|
const departments = roleRows.departments;
|
|
12413
12933
|
const { usedLinkage, resolvedCode } = await resolveAllocationCode(http, rid, linkage, pins, departments);
|
|
12414
12934
|
const defaults = windowDefaults(params);
|
|
@@ -12431,12 +12951,12 @@ function discoveryField(err, body, defaults) {
|
|
|
12431
12951
|
}
|
|
12432
12952
|
|
|
12433
12953
|
// src/resources/special/time_entries/move.ts
|
|
12434
|
-
var
|
|
12954
|
+
var OP6 = "time_entries.moveToTicket";
|
|
12435
12955
|
var COPY2 = ["resourceID", "roleID", "billingCodeID", "dateWorked", "startDateTime", "endDateTime", "hoursWorked", "offsetHours", "summaryNotes", "internalNotes", "isNonBillable", "showOnInvoice"];
|
|
12436
12956
|
var CONTRACT_SCOPED2 = ["contractID", "contractServiceID", "contractServiceBundleID"];
|
|
12437
12957
|
async function readTimeEntry(http, id, retries) {
|
|
12438
12958
|
try {
|
|
12439
|
-
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 });
|
|
12440
12960
|
return res?.item ?? null;
|
|
12441
12961
|
} catch (e) {
|
|
12442
12962
|
if (isAutotaskError(e) && (e.httpStatus === 404 || e.code === "NOT_FOUND")) return null;
|
|
@@ -12446,27 +12966,27 @@ async function readTimeEntry(http, id, retries) {
|
|
|
12446
12966
|
async function checkSource2({ http, source, sourceId, toTicketId, retries }) {
|
|
12447
12967
|
const ids2 = { sourceId, toTicketId };
|
|
12448
12968
|
if (source.taskID !== null && source.taskID !== void 0) {
|
|
12449
|
-
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);
|
|
12450
12970
|
}
|
|
12451
12971
|
if (source.billingApprovalDateTime || source.billingApprovalResourceID || source.billingApprovalLevelMostRecent) {
|
|
12452
|
-
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" } });
|
|
12453
12973
|
}
|
|
12454
12974
|
let billing;
|
|
12455
12975
|
try {
|
|
12456
|
-
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);
|
|
12457
12977
|
} catch {
|
|
12458
|
-
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" } });
|
|
12459
12979
|
}
|
|
12460
12980
|
if (billing.some((b) => b.invoiceID || b.postedDate)) {
|
|
12461
|
-
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" } });
|
|
12462
12982
|
}
|
|
12463
|
-
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);
|
|
12464
12984
|
if (attachments.length > 0) {
|
|
12465
|
-
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" } });
|
|
12466
12986
|
}
|
|
12467
12987
|
}
|
|
12468
12988
|
var SPEC2 = {
|
|
12469
|
-
operation:
|
|
12989
|
+
operation: OP6,
|
|
12470
12990
|
entity: "time_entry",
|
|
12471
12991
|
readSource: readTimeEntry,
|
|
12472
12992
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -12500,29 +13020,116 @@ function moveToTicket2(_resource, sourceId, toTicketId, opts, http) {
|
|
|
12500
13020
|
return runMove(http, SPEC2, sourceId, toTicketId, opts);
|
|
12501
13021
|
}
|
|
12502
13022
|
|
|
13023
|
+
// src/resources/special/time_entries/approve.ts
|
|
13024
|
+
var OP7 = "time_entries.approve";
|
|
13025
|
+
var APPROVAL_LEVELS_PATH = "/V1.0/BillingItemApprovalLevels";
|
|
13026
|
+
function toIso(value) {
|
|
13027
|
+
if (value instanceof Date) {
|
|
13028
|
+
if (Number.isNaN(value.getTime())) throw new AutotaskConfigError("time_entries.approve: approvalDateTime is an invalid Date");
|
|
13029
|
+
return value.toISOString();
|
|
13030
|
+
}
|
|
13031
|
+
const parsed = new Date(value);
|
|
13032
|
+
if (Number.isNaN(parsed.getTime())) throw new AutotaskConfigError(`time_entries.approve: approvalDateTime "${value}" is not a parseable date`);
|
|
13033
|
+
return parsed.toISOString();
|
|
13034
|
+
}
|
|
13035
|
+
function toNumber(value) {
|
|
13036
|
+
return typeof value === "number" ? value : Number(value);
|
|
13037
|
+
}
|
|
13038
|
+
function sameInstant(a, b) {
|
|
13039
|
+
if (typeof a !== "string" || a === "") return false;
|
|
13040
|
+
const left = new Date(a).getTime();
|
|
13041
|
+
const right = new Date(b).getTime();
|
|
13042
|
+
return !Number.isNaN(left) && left === right;
|
|
13043
|
+
}
|
|
13044
|
+
async function approve(resource, id, opts = {}, http) {
|
|
13045
|
+
const numericId = toNumber(parseIdentifier(id, "id"));
|
|
13046
|
+
if (opts.approvalLevel !== void 0 && (!Number.isInteger(opts.approvalLevel) || opts.approvalLevel < 1)) {
|
|
13047
|
+
throw new AutotaskConfigError(`time_entries.approve: approvalLevel must be a positive integer (got ${String(opts.approvalLevel)})`);
|
|
13048
|
+
}
|
|
13049
|
+
const approvalLevel = opts.approvalLevel ?? 1;
|
|
13050
|
+
const approvalDateTime = toIso(opts.approvalDateTime ?? /* @__PURE__ */ new Date());
|
|
13051
|
+
const entry = await resource.get(numericId);
|
|
13052
|
+
const approvalResourceID = opts.approvalResourceID === void 0 ? entry.resourceID : toNumber(parseIdentifier(opts.approvalResourceID, "approvalResourceID"));
|
|
13053
|
+
if (approvalResourceID === void 0) {
|
|
13054
|
+
throw new AutotaskConfigError(
|
|
13055
|
+
`time_entries.approve: time entry ${numericId} carries no resourceID and the caller supplied no approvalResourceID \u2014 the vendor requires an approving resource`
|
|
13056
|
+
);
|
|
13057
|
+
}
|
|
13058
|
+
const body = { timeEntryID: numericId, approvalLevel, approvalResourceID, approvalDateTime };
|
|
13059
|
+
if (opts.dryRun === true) {
|
|
13060
|
+
return buildDryRunResult(
|
|
13061
|
+
{
|
|
13062
|
+
operation: OP7,
|
|
13063
|
+
method: "POST",
|
|
13064
|
+
path: APPROVAL_LEVELS_PATH,
|
|
13065
|
+
ids: [numericId],
|
|
13066
|
+
checks: [
|
|
13067
|
+
{ name: "input-validation", ok: true },
|
|
13068
|
+
{ name: "time-entry-exists", ok: true }
|
|
13069
|
+
],
|
|
13070
|
+
impact: { affected: 1, scope: "single-record", reversible: false },
|
|
13071
|
+
warnings: [
|
|
13072
|
+
"two calls: POST the BillingItemApprovalLevels record, then PUT the time entry (billingApprovalDateTime + billingApprovalResourceID) \u2014 the vendor sets BillingApprovalLevelMostRecent from the create but not these two fields",
|
|
13073
|
+
"approval is not posting: the API exposes no post action on BillingItems, so the entry stays unposted until a vendor workflow posts it",
|
|
13074
|
+
`approvalResourceID ${approvalResourceID} is not existence-checked by the dry run`
|
|
13075
|
+
],
|
|
13076
|
+
data: body
|
|
13077
|
+
},
|
|
13078
|
+
{ redactPersonalData: http.redactPersonalData }
|
|
13079
|
+
);
|
|
13080
|
+
}
|
|
13081
|
+
const created = await http.request({
|
|
13082
|
+
method: "POST",
|
|
13083
|
+
path: APPROVAL_LEVELS_PATH,
|
|
13084
|
+
body,
|
|
13085
|
+
operation: OP7,
|
|
13086
|
+
maxRetries: opts.maxRetries
|
|
13087
|
+
});
|
|
13088
|
+
const levelId = toNumber(parseIdentifier(unwrapOperation(created, OP7).id, "billingItemApprovalLevelID"));
|
|
13089
|
+
await resource.update(
|
|
13090
|
+
numericId,
|
|
13091
|
+
{ billingApprovalDateTime: approvalDateTime, billingApprovalResourceID: approvalResourceID },
|
|
13092
|
+
{ maxRetries: opts.maxRetries, lastModifiedDate: opts.lastModifiedDate ?? entry.lastModifiedDateTime }
|
|
13093
|
+
);
|
|
13094
|
+
const fresh = await resource.get(numericId);
|
|
13095
|
+
const persisted = sameInstant(fresh.billingApprovalDateTime, approvalDateTime) && fresh.billingApprovalResourceID === approvalResourceID;
|
|
13096
|
+
return {
|
|
13097
|
+
timeEntryID: numericId,
|
|
13098
|
+
billingItemApprovalLevelID: levelId,
|
|
13099
|
+
approvalLevel,
|
|
13100
|
+
approvalResourceID,
|
|
13101
|
+
approvalDateTime,
|
|
13102
|
+
billingApprovalLevelMostRecent: fresh.billingApprovalLevelMostRecent ?? null,
|
|
13103
|
+
billingApprovalDateTime: fresh.billingApprovalDateTime ?? null,
|
|
13104
|
+
billingApprovalResourceID: fresh.billingApprovalResourceID ?? null,
|
|
13105
|
+
status: persisted ? "approved" : "approval_fields_not_persisted",
|
|
13106
|
+
persisted
|
|
13107
|
+
};
|
|
13108
|
+
}
|
|
13109
|
+
|
|
12503
13110
|
// src/resources/special/time_entries/recentTickets.ts
|
|
12504
|
-
var
|
|
13111
|
+
var OP8 = "time_entries.recentTickets";
|
|
12505
13112
|
var DAY_MS3 = 864e5;
|
|
12506
|
-
var
|
|
12507
|
-
var
|
|
12508
|
-
function
|
|
13113
|
+
var TICKET_CHUNK2 = 100;
|
|
13114
|
+
var TICKET_FIELDS3 = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
13115
|
+
function positiveId2(value, name) {
|
|
12509
13116
|
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
12510
13117
|
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
12511
|
-
throw new AutotaskConfigError(`${
|
|
13118
|
+
throw new AutotaskConfigError(`${OP8}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP8 });
|
|
12512
13119
|
}
|
|
12513
13120
|
return n;
|
|
12514
13121
|
}
|
|
12515
13122
|
async function recentTickets(_resource, from, to, http, opts) {
|
|
12516
13123
|
if (opts === void 0 || opts === null || typeof opts !== "object") {
|
|
12517
|
-
throw new AutotaskConfigError(`${
|
|
13124
|
+
throw new AutotaskConfigError(`${OP8}: opts { resourceID, companyID?, tz?, limit? } is required`, { operation: OP8 });
|
|
12518
13125
|
}
|
|
12519
|
-
const resourceID =
|
|
12520
|
-
const companyID = opts.companyID === void 0 ? void 0 :
|
|
12521
|
-
if (opts.tz !== void 0) validateZone(opts.tz,
|
|
12522
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
12523
|
-
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);
|
|
12524
13131
|
if (fromB.ms > toB.ms) {
|
|
12525
|
-
throw new AutotaskConfigError(`${
|
|
13132
|
+
throw new AutotaskConfigError(`${OP8}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`, { operation: OP8 });
|
|
12526
13133
|
}
|
|
12527
13134
|
const windowToMs = toB.ms + DAY_MS3 - 1;
|
|
12528
13135
|
const { items: entries, hasMore } = await queryTimeEntries(
|
|
@@ -12533,7 +13140,7 @@ async function recentTickets(_resource, from, to, http, opts) {
|
|
|
12533
13140
|
{ field: "resourceID", op: "eq", value: resourceID }
|
|
12534
13141
|
],
|
|
12535
13142
|
clampWindowLimit(opts.limit),
|
|
12536
|
-
|
|
13143
|
+
OP8,
|
|
12537
13144
|
http
|
|
12538
13145
|
);
|
|
12539
13146
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -12551,17 +13158,17 @@ async function recentTickets(_resource, from, to, http, opts) {
|
|
|
12551
13158
|
}
|
|
12552
13159
|
const ids2 = [...groups.keys()];
|
|
12553
13160
|
const tickets = /* @__PURE__ */ new Map();
|
|
12554
|
-
for (let c = 0; c < ids2.length; c +=
|
|
12555
|
-
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);
|
|
12556
13163
|
const filter = [{ field: "id", op: "in", value: chunk }];
|
|
12557
13164
|
const res = await http.request({
|
|
12558
13165
|
method: "POST",
|
|
12559
13166
|
idempotent: true,
|
|
12560
13167
|
path: "/V1.0/Tickets/query",
|
|
12561
|
-
body: { MaxRecords:
|
|
12562
|
-
operation:
|
|
13168
|
+
body: { MaxRecords: TICKET_CHUNK2, filter, IncludeFields: TICKET_FIELDS3 },
|
|
13169
|
+
operation: OP8
|
|
12563
13170
|
});
|
|
12564
|
-
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);
|
|
12565
13172
|
}
|
|
12566
13173
|
const items = [];
|
|
12567
13174
|
const unresolvedTicketIDs = [];
|
|
@@ -12665,6 +13272,10 @@ var TimeEntriesResource = class extends BaseResource {
|
|
|
12665
13272
|
return this.resolveBase(identifier);
|
|
12666
13273
|
}
|
|
12667
13274
|
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
|
|
13275
|
+
approve(id, opts = {}) {
|
|
13276
|
+
return approve(this, id, opts, this.http);
|
|
13277
|
+
}
|
|
13278
|
+
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
|
|
12668
13279
|
createForResource(resourceID, params, opts = {}) {
|
|
12669
13280
|
return createForResource(this, resourceID, params, opts, this.http);
|
|
12670
13281
|
}
|
|
@@ -12805,7 +13416,7 @@ var TimeEntryAttachmentsResource = class extends BaseResource {
|
|
|
12805
13416
|
};
|
|
12806
13417
|
|
|
12807
13418
|
// src/resources/special/time_off_requests.ts
|
|
12808
|
-
async function
|
|
13419
|
+
async function approve2(_resource, id, opts = {}, http) {
|
|
12809
13420
|
const numericId = parseIdentifier(id, "id");
|
|
12810
13421
|
if (opts.dryRun === true) {
|
|
12811
13422
|
return buildDryRunResult({
|
|
@@ -12893,7 +13504,7 @@ var TimeOffRequestsResource = class extends BaseResource {
|
|
|
12893
13504
|
}
|
|
12894
13505
|
/** Node-parity special op (hand-written in src/resources/special/time_off_requests.ts). */
|
|
12895
13506
|
approve(id, opts = {}) {
|
|
12896
|
-
return
|
|
13507
|
+
return approve2(this, id, opts, this.http);
|
|
12897
13508
|
}
|
|
12898
13509
|
/** Node-parity special op (hand-written in src/resources/special/time_off_requests.ts). */
|
|
12899
13510
|
reject(id, reason, opts = {}) {
|
|
@@ -12965,6 +13576,7 @@ export {
|
|
|
12965
13576
|
AppointmentsResource,
|
|
12966
13577
|
AuthenticateResource,
|
|
12967
13578
|
BillingCodesResource,
|
|
13579
|
+
BillingItemApprovalLevelsResource,
|
|
12968
13580
|
BillingItemsResource,
|
|
12969
13581
|
ChangeRequestLinksResource,
|
|
12970
13582
|
ChecklistLibrariesResource,
|
|
@@ -13024,11 +13636,15 @@ export {
|
|
|
13024
13636
|
HolidaysResource,
|
|
13025
13637
|
InventoryProductsResource,
|
|
13026
13638
|
InvoicesResource,
|
|
13639
|
+
InvoiceTemplatesResource,
|
|
13027
13640
|
ModulesResource,
|
|
13028
13641
|
NotificationHistoryResource,
|
|
13029
13642
|
OpportunitiesResource,
|
|
13030
13643
|
OpportunityAttachmentsResource,
|
|
13031
13644
|
OpportunityCategoriesResource,
|
|
13645
|
+
OrganizationalLevel1sResource,
|
|
13646
|
+
OrganizationalLevel2sResource,
|
|
13647
|
+
OrganizationalLevelAssociationsResource,
|
|
13032
13648
|
PhasesResource,
|
|
13033
13649
|
ProductsResource,
|
|
13034
13650
|
ProductVendorsResource,
|
|
@@ -13051,6 +13667,7 @@ export {
|
|
|
13051
13667
|
ResourceTimeOffApproversResource,
|
|
13052
13668
|
ResourceTimeOffBalancesResource,
|
|
13053
13669
|
RolesResource,
|
|
13670
|
+
SalesOrdersResource,
|
|
13054
13671
|
ServiceCallsResource,
|
|
13055
13672
|
ServiceCallTaskResourcesResource,
|
|
13056
13673
|
ServiceCallTasksResource,
|
|
@@ -13068,6 +13685,7 @@ export {
|
|
|
13068
13685
|
TagsResource,
|
|
13069
13686
|
TaskSecondaryResourcesResource,
|
|
13070
13687
|
TasksResource,
|
|
13688
|
+
TaxRegionsResource,
|
|
13071
13689
|
ThresholdInformationResource,
|
|
13072
13690
|
TicketAdditionalConfigurationItemsResource,
|
|
13073
13691
|
TicketAdditionalContactsResource,
|