node-autotask 0.31.0 → 0.33.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 +38 -0
- package/MCP_TOOL_CATALOG.json +31 -12
- package/MCP_TOOL_MANIFEST.md +18 -19
- package/MCP_TOOL_OVERRIDES.json +0 -12
- package/N8N_MCP_MANIFEST.json +44 -18
- package/N8N_MCP_MANIFEST.md +15 -14
- package/README.md +8 -6
- package/capabilities.json +1579 -1414
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-3H3RJ4LA.cjs → chunk-3LAZZT7V.cjs} +143 -143
- package/dist/{chunk-PCLL536B.cjs → chunk-4QN43TRP.cjs} +75 -50
- package/dist/{chunk-6UA544OR.js → chunk-5DS6F6GF.js} +299 -113
- package/dist/{chunk-RQZUV74N.js → chunk-7WNE5MPG.js} +2 -2
- package/dist/{chunk-INSFDXHP.js → chunk-MFDQHFGJ.js} +28 -3
- package/dist/{chunk-DQLEVKZS.cjs → chunk-OO67MRHN.cjs} +814 -628
- package/dist/{chunk-QXUN2VCJ.cjs → chunk-SP2SJJZX.cjs} +1578 -1413
- package/dist/{chunk-EPOHRNSX.js → chunk-VKE56TTN.js} +1578 -1413
- package/dist/{index-Uqj987EE.d.ts → index-BaeMHZPk.d.ts} +149 -12
- package/dist/{index--5jrRl4u.d.cts → index-Cq0VsfSz.d.cts} +149 -12
- 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 +120 -133
- package/dist/operations/index.d.cts +2 -2
- package/dist/operations/index.d.ts +2 -2
- package/dist/operations/index.js +3 -16
- 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 +62 -37
- package/package.json +2 -1
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
createNamePlan,
|
|
26
26
|
cursorToPath,
|
|
27
27
|
decodeFileData,
|
|
28
|
+
effectiveRequiredFields,
|
|
28
29
|
formatUnknownNameList,
|
|
29
30
|
formatVendorDetail,
|
|
30
31
|
isAutotaskError,
|
|
@@ -40,10 +41,10 @@ import {
|
|
|
40
41
|
unwrapItems,
|
|
41
42
|
unwrapOperation,
|
|
42
43
|
vendorId
|
|
43
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-MFDQHFGJ.js";
|
|
44
45
|
import {
|
|
45
46
|
CAPABILITIES
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-VKE56TTN.js";
|
|
47
48
|
|
|
48
49
|
// src/resources/appointments.ts
|
|
49
50
|
var AppointmentsResource = class extends BaseResource {
|
|
@@ -273,11 +274,11 @@ function tzOffsetMs(tz, ms) {
|
|
|
273
274
|
}
|
|
274
275
|
function localDayStartMs(y, m, d, tz) {
|
|
275
276
|
if (tz === void 0) return Date.UTC(y, m - 1, d);
|
|
276
|
-
const
|
|
277
|
-
const offset = tzOffsetMs(tz,
|
|
278
|
-
let start =
|
|
277
|
+
const candidate2 = Date.UTC(y, m - 1, d);
|
|
278
|
+
const offset = tzOffsetMs(tz, candidate2);
|
|
279
|
+
let start = candidate2 - offset;
|
|
279
280
|
if (tzOffsetMs(tz, start) !== offset) {
|
|
280
|
-
start =
|
|
281
|
+
start = candidate2 - tzOffsetMs(tz, start);
|
|
281
282
|
}
|
|
282
283
|
const p = wallParts(tz, start);
|
|
283
284
|
if (p.d !== d) start += 864e5 - ((p.h * 60 + p.mi) * 60 + p.s) * 1e3;
|
|
@@ -446,7 +447,7 @@ function dateOnlyWindowBounds(kind, now, tz) {
|
|
|
446
447
|
}
|
|
447
448
|
async function dateWindowQuery(http, path, field, kind, limit, operation, now, tz) {
|
|
448
449
|
const zone = tz === void 0 ? void 0 : validateZone(tz, operation);
|
|
449
|
-
const { from, to } =
|
|
450
|
+
const { from, to } = dateOnlyWindowBounds(kind, now, zone);
|
|
450
451
|
const filter = [
|
|
451
452
|
{ field, op: "gte", value: from },
|
|
452
453
|
{ field, op: "lte", value: to }
|
|
@@ -484,15 +485,16 @@ function customRangeDay(value, label, zone, opName) {
|
|
|
484
485
|
return { y, mo, d, ms };
|
|
485
486
|
}
|
|
486
487
|
async function customRangeQuery(http, path, field, from, to, limit, operation, zone) {
|
|
487
|
-
|
|
488
|
-
const
|
|
488
|
+
if (zone !== void 0) validateZone(zone, operation);
|
|
489
|
+
const fromB = customRangeDay(from, "from", void 0, operation);
|
|
490
|
+
const toB = customRangeDay(to, "to", void 0, operation);
|
|
489
491
|
if (fromB.ms > toB.ms) {
|
|
490
492
|
throw new AutotaskConfigError(
|
|
491
493
|
`${operation}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`,
|
|
492
494
|
{ operation }
|
|
493
495
|
);
|
|
494
496
|
}
|
|
495
|
-
const toEndMs =
|
|
497
|
+
const toEndMs = toB.ms + DAY_MS - 1;
|
|
496
498
|
const filter = [
|
|
497
499
|
{ field, op: "gte", value: isoUtc(fromB.ms) },
|
|
498
500
|
{ field, op: "lte", value: isoUtc(toEndMs) }
|
|
@@ -1560,6 +1562,151 @@ async function fullProfile(resource, id, opts = {}, http) {
|
|
|
1560
1562
|
return result;
|
|
1561
1563
|
}
|
|
1562
1564
|
|
|
1565
|
+
// src/resources/special/companies/prepareCrmWrite.ts
|
|
1566
|
+
var FREEMAIL = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com", "yahoo.com", "yahoo.com.au", "icloud.com", "bigpond.com", "optusnet.com.au", "me.com", "proton.me", "protonmail.com"]);
|
|
1567
|
+
var LIMIT = 10;
|
|
1568
|
+
var OP2 = "companies.prepareCrmWrite";
|
|
1569
|
+
async function readRows(http, path, filter) {
|
|
1570
|
+
const res = await http.request({ method: "POST", idempotent: true, path, body: { MaxRecords: LIMIT * 5, filter }, operation: OP2 });
|
|
1571
|
+
return unwrapItems(res).items;
|
|
1572
|
+
}
|
|
1573
|
+
var str = (v) => typeof v === "string" && v !== "" ? v : null;
|
|
1574
|
+
var bool = (v) => typeof v === "boolean" ? v : null;
|
|
1575
|
+
var norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\b(pty|ltd|limited|inc|llc)\b/g, "").replace(/\s+/g, " ").trim();
|
|
1576
|
+
function domainOf(input) {
|
|
1577
|
+
if (!input) return null;
|
|
1578
|
+
const host = input.trim().toLowerCase().replace(/^[a-z]+:\/\//, "").replace(/^.*@/, "").replace(/^www\./, "").split(/[/?#:]/)[0] ?? "";
|
|
1579
|
+
return host.includes(".") && !FREEMAIL.has(host) ? host : null;
|
|
1580
|
+
}
|
|
1581
|
+
function candidate(r) {
|
|
1582
|
+
return { id: r.id, companyName: str(r.companyName), webAddress: str(r.webAddress), phone: str(r.phone), isActive: bool(r.isActive) };
|
|
1583
|
+
}
|
|
1584
|
+
function has(v) {
|
|
1585
|
+
return v !== void 0 && v !== null && v !== "";
|
|
1586
|
+
}
|
|
1587
|
+
async function prepareCrmWrite(resource, input, http) {
|
|
1588
|
+
const intents = ["create_company", "add_contact", "update_contact", "update_company"];
|
|
1589
|
+
if (!input || !intents.includes(input.intent)) throw new AutotaskConfigError(`prepareCrmWrite: intent must be one of ${intents.join(", ")}`);
|
|
1590
|
+
const co = input.company ?? {};
|
|
1591
|
+
const ct = input.contact ?? {};
|
|
1592
|
+
const caveats = [
|
|
1593
|
+
"Companies, resources and countries are blind surfaces on some tenants: a miss is not proof a record is absent, and owner / country candidates are lookups, not lists.",
|
|
1594
|
+
"Company name matching is `contains` (exact names ranked first); there is no semantic search in the SDK."
|
|
1595
|
+
];
|
|
1596
|
+
const needsInput = [];
|
|
1597
|
+
let candidates = [];
|
|
1598
|
+
let matchedBy = null;
|
|
1599
|
+
let searched = false;
|
|
1600
|
+
if (has(co.id)) {
|
|
1601
|
+
searched = true;
|
|
1602
|
+
const rows = await readRows(http, "/V1.0/Companies/query", [{ field: "id", op: "eq", value: co.id }]);
|
|
1603
|
+
candidates = rows.map(candidate);
|
|
1604
|
+
matchedBy = candidates.length ? "id" : null;
|
|
1605
|
+
}
|
|
1606
|
+
const domain = domainOf(co.domain) ?? domainOf(co.website) ?? domainOf(ct.emailAddress);
|
|
1607
|
+
if (candidates.length === 0 && domain) {
|
|
1608
|
+
searched = true;
|
|
1609
|
+
candidates = (await resource.searchByDomain(domain, { limit: LIMIT })).map((c) => candidate(c));
|
|
1610
|
+
matchedBy = candidates.length ? "domain" : null;
|
|
1611
|
+
}
|
|
1612
|
+
if (candidates.length === 0 && has(co.companyName)) {
|
|
1613
|
+
searched = true;
|
|
1614
|
+
const rows = (await resource.searchByIdentity({ companyName: co.companyName }, { limit: LIMIT })).map((c) => candidate(c));
|
|
1615
|
+
const want = norm(co.companyName);
|
|
1616
|
+
const exact = rows.filter((r) => r.companyName !== null && norm(r.companyName) === want);
|
|
1617
|
+
candidates = exact.length ? exact : rows;
|
|
1618
|
+
matchedBy = candidates.length ? exact.length ? "name_exact" : "name_contains" : null;
|
|
1619
|
+
}
|
|
1620
|
+
const status = !searched ? "not_searched" : candidates.length === 0 ? "none" : candidates.length === 1 ? "matched" : "ambiguous";
|
|
1621
|
+
const companyID = status === "matched" ? candidates[0].id : null;
|
|
1622
|
+
const company = { status, matchedBy, candidates };
|
|
1623
|
+
if (status === "ambiguous") {
|
|
1624
|
+
needsInput.push({ field: "company", why: `${candidates.length} companies match; choose one`, source: "policy", lookup: { candidates: candidates.map((c) => ({ id: c.id, companyName: c.companyName })) }, research: "ask" });
|
|
1625
|
+
}
|
|
1626
|
+
if (status === "none" && input.intent !== "create_company") {
|
|
1627
|
+
needsInput.push({ field: "company", why: "no company matched; give a company id, name or domain, or use intent create_company (a miss is not proof of absence)", source: "policy", research: "ask" });
|
|
1628
|
+
}
|
|
1629
|
+
if (status === "not_searched" && input.intent !== "create_company") {
|
|
1630
|
+
needsInput.push({ field: "company", why: "give a company id, name, domain/website or a contact email to search by", source: "policy", research: "ask" });
|
|
1631
|
+
}
|
|
1632
|
+
let locations = null;
|
|
1633
|
+
if (companyID !== null) {
|
|
1634
|
+
const rows = await readRows(http, "/V1.0/CompanyLocations/query", [{ field: "companyID", op: "eq", value: companyID }]);
|
|
1635
|
+
const items = rows.map((r) => ({ id: r.id, name: str(r.name), isPrimary: bool(r.isPrimary), isActive: bool(r.isActive), city: str(r.city), state: str(r.state), postalCode: str(r.postalCode) }));
|
|
1636
|
+
const active = items.filter((l) => l.isActive !== false);
|
|
1637
|
+
const m = active.filter((l) => has(ct.city) && l.city?.toLowerCase() === String(ct.city).toLowerCase() || has(ct.postalCode) && l.postalCode === String(ct.postalCode));
|
|
1638
|
+
const pick = has(ct.companyLocationID) ? { rule: "matched", locationID: ct.companyLocationID } : active.length === 1 ? { rule: "single", locationID: active[0].id } : active.length > 1 && m.length === 1 ? { rule: "matched", locationID: m[0].id } : active.length > 1 ? { rule: "ask", locationID: null } : { rule: "none", locationID: null };
|
|
1639
|
+
locations = { items, pick };
|
|
1640
|
+
if (input.intent === "add_contact" && pick.locationID === null) {
|
|
1641
|
+
needsInput.push({
|
|
1642
|
+
field: "companyLocationID",
|
|
1643
|
+
why: active.length > 1 ? "the company has several active locations; do not guess" : "the company has no active location; create or reactivate one first",
|
|
1644
|
+
source: "policy",
|
|
1645
|
+
lookup: { candidates: active.map((l) => ({ id: l.id, name: l.name, isPrimary: l.isPrimary })) },
|
|
1646
|
+
research: "ask"
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
let contacts = null;
|
|
1651
|
+
if (input.intent !== "create_company" && (has(ct.emailAddress) || has(ct.firstName) && has(ct.lastName) || has(ct.id))) {
|
|
1652
|
+
const pickFields = (r) => ({ id: r.id, firstName: r.firstName, lastName: r.lastName, emailAddress: r.emailAddress, companyID: r.companyID, companyLocationID: r.companyLocationID, isActive: r.isActive });
|
|
1653
|
+
let rows = [];
|
|
1654
|
+
let by = null;
|
|
1655
|
+
if (has(ct.id)) rows = await readRows(http, "/V1.0/Contacts/query", [{ field: "id", op: "eq", value: ct.id }]);
|
|
1656
|
+
else if (has(ct.emailAddress)) {
|
|
1657
|
+
rows = await readRows(http, "/V1.0/Contacts/query", [{ field: "emailAddress", op: "eq", value: ct.emailAddress }]);
|
|
1658
|
+
by = rows.length ? "email" : null;
|
|
1659
|
+
}
|
|
1660
|
+
if (rows.length === 0 && has(ct.firstName) && has(ct.lastName)) {
|
|
1661
|
+
const f = [{ field: "firstName", op: "eq", value: ct.firstName }, { field: "lastName", op: "eq", value: ct.lastName }];
|
|
1662
|
+
if (companyID !== null) f.push({ field: "companyID", op: "eq", value: companyID });
|
|
1663
|
+
rows = await readRows(http, "/V1.0/Contacts/query", f);
|
|
1664
|
+
by = rows.length ? "name" : null;
|
|
1665
|
+
}
|
|
1666
|
+
contacts = { duplicates: rows.slice(0, LIMIT).map(pickFields), matchedBy: by };
|
|
1667
|
+
if (input.intent === "update_contact" && rows.length === 0) {
|
|
1668
|
+
needsInput.push({ field: "contact", why: "no contact matched; give a contact id or email", source: "policy", research: "ask" });
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (input.intent === "create_company" && status !== "matched") {
|
|
1672
|
+
const eff = effectiveRequiredFields(null, RELATIONSHIP_MAP.companies?.zoneFacts);
|
|
1673
|
+
const research = (f) => ["companyType", "ownerResourceID", "countryID"].includes(f) ? "lookup" : "web_if_available_else_ask";
|
|
1674
|
+
const lookups = {
|
|
1675
|
+
companyType: { via: "operations.fieldCatalog", resource: "companies", field: "companyType", labels: true, note: "map the user wording to one returned label; never hardcode ids" },
|
|
1676
|
+
ownerResourceID: { via: "resources.get / the staff resolver", note: "owners cannot be enumerated on a blind surface: take a known resource id and confirm it with resources.get" },
|
|
1677
|
+
countryID: { via: "countries.query", note: "resolve the country name to an id; never guess" }
|
|
1678
|
+
};
|
|
1679
|
+
const wanted = [
|
|
1680
|
+
...eff.fields.filter((f) => f !== "isActive").map((f) => [f, "live-proven"]),
|
|
1681
|
+
...["address1", "city", "state", "postalCode", "countryID"].map((f) => [f, "policy"])
|
|
1682
|
+
];
|
|
1683
|
+
for (const [f, source] of wanted) {
|
|
1684
|
+
if (has(co[f]) || needsInput.some((n) => n.field === f)) continue;
|
|
1685
|
+
needsInput.push({ field: f, why: source === "policy" ? `CRM policy: a new company needs ${f} (the vendor auto-creates a "Primary Address" location from it)` : `the vendor requires ${f} to create a company`, source, ...lookups[f] ? { lookup: lookups[f] } : {}, research: research(f) });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (input.intent === "add_contact") {
|
|
1689
|
+
for (const f of ["firstName", "lastName"]) if (!has(ct[f])) needsInput.push({ field: f, why: `the vendor requires contact ${f}`, source: "live-proven", research: "ask" });
|
|
1690
|
+
if (!has(ct.emailAddress) && !has(ct.phone) && !has(ct.mobilePhone)) needsInput.push({ field: "emailAddress", why: "CRM policy: a contact needs an email or a phone number", source: "policy", research: "ask" });
|
|
1691
|
+
}
|
|
1692
|
+
let next = null;
|
|
1693
|
+
const isInactive = (d) => d.isActive === false || d.isActive === 0;
|
|
1694
|
+
const dups = contacts?.duplicates ?? [];
|
|
1695
|
+
const sameCompany = companyID === null ? dups : dups.filter((d) => d.companyID === companyID);
|
|
1696
|
+
const elsewhere = companyID === null ? [] : dups.filter((d) => d.companyID !== companyID && !isInactive(d));
|
|
1697
|
+
if (input.intent === "add_contact" && sameCompany.length === 0 && elsewhere.length > 0) {
|
|
1698
|
+
needsInput.push({ field: "contact", why: `a contact with this ${contacts?.matchedBy ?? "identity"} is active at another company: update it, move it with contacts.moveToCompany, or confirm a second record`, source: "policy", lookup: { duplicates: elsewhere }, research: "ask" });
|
|
1699
|
+
}
|
|
1700
|
+
if (needsInput.length === 0) {
|
|
1701
|
+
const dup = sameCompany[0];
|
|
1702
|
+
if (input.intent === "create_company") next = status === "matched" ? { operation: "companies.update", note: "the company already exists: update it instead of creating" } : { operation: "companies.create", note: 'then re-run with intent add_contact: the vendor auto-creates a "Primary Address" location (rename it with company_locations.update; locations cannot be deleted via the API)' };
|
|
1703
|
+
else if (input.intent === "add_contact") next = dup ? { operation: "contacts.update", note: `a contact already matches by ${contacts?.matchedBy ?? "id"} (id ${String(dup.id)}): update it, or move it with contacts.moveToCompany if it belongs to another company` } : { operation: "contacts.create", note: `set companyID ${String(companyID)} and companyLocationID ${String(locations?.pick.locationID)}` };
|
|
1704
|
+
else if (input.intent === "update_contact") next = { operation: "contacts.update", note: "send only the changed fields" };
|
|
1705
|
+
else next = { operation: "companies.update", note: "send only the changed fields" };
|
|
1706
|
+
}
|
|
1707
|
+
return { intent: input.intent, company, locations, contacts, needsInput, next, caveats };
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1563
1710
|
// src/resources/special/companies/unbilledWorkTypes.ts
|
|
1564
1711
|
var UNBILLED_WORK_NOTE_CHARS_DEFAULT = 400;
|
|
1565
1712
|
var UNBILLED_WORK_JOIN_SET_CAP = 300;
|
|
@@ -1670,7 +1817,7 @@ function validateUnbilledWorkOptions(companyId, opts) {
|
|
|
1670
1817
|
}
|
|
1671
1818
|
|
|
1672
1819
|
// src/resources/special/companies/unbilledWork.ts
|
|
1673
|
-
var
|
|
1820
|
+
var OP3 = "companies.unbilledWork";
|
|
1674
1821
|
function capJoinSet(ids2, cap) {
|
|
1675
1822
|
const sorted = [...new Set(ids2)].sort((a, b) => a - b);
|
|
1676
1823
|
if (sorted.length <= cap) return { ids: sorted, truncated: false };
|
|
@@ -1707,14 +1854,14 @@ async function readCompanyAndParent(resource, id, includeParent) {
|
|
|
1707
1854
|
}
|
|
1708
1855
|
async function readJoinSets(http, companyIds, v) {
|
|
1709
1856
|
const ticketsCond = [{ field: "companyID", op: "in", value: companyIds }];
|
|
1710
|
-
const ticketsRaw = await boundedPart(http, "/V1.0/Tickets/query", ticketsCond, v.limit,
|
|
1857
|
+
const ticketsRaw = await boundedPart(http, "/V1.0/Tickets/query", ticketsCond, v.limit, OP3);
|
|
1711
1858
|
const projectsCond = [{ field: "companyID", op: "in", value: companyIds }];
|
|
1712
|
-
const projectsRaw = await boundedPart(http, "/V1.0/Projects/query", projectsCond, v.limit,
|
|
1859
|
+
const projectsRaw = await boundedPart(http, "/V1.0/Projects/query", projectsCond, v.limit, OP3);
|
|
1713
1860
|
const projectIds = projectsRaw.items.map((p) => num(p.id)).filter((n) => n > 0);
|
|
1714
1861
|
let taskIds = [];
|
|
1715
1862
|
let tasksHasMore = false;
|
|
1716
1863
|
if (projectIds.length > 0) {
|
|
1717
|
-
const tasksRaw = await boundedPart(http, "/V1.0/Tasks/query", [{ field: "projectID", op: "in", value: projectIds }], v.limit,
|
|
1864
|
+
const tasksRaw = await boundedPart(http, "/V1.0/Tasks/query", [{ field: "projectID", op: "in", value: projectIds }], v.limit, OP3);
|
|
1718
1865
|
taskIds = tasksRaw.items.map((t) => num(t.id)).filter((n) => n > 0);
|
|
1719
1866
|
tasksHasMore = tasksRaw.hasMore;
|
|
1720
1867
|
}
|
|
@@ -1738,7 +1885,7 @@ async function readBillingPart(http, v, companyIds) {
|
|
|
1738
1885
|
}
|
|
1739
1886
|
const billingFields = resolveFieldsPreset("billing_join", "billing_items");
|
|
1740
1887
|
const billingIncludeFields = [...billingFields, "id", ...v.includeNotes ? ["description"] : []];
|
|
1741
|
-
const billingRaw = await boundedPart(http, "/V1.0/BillingItems/query", billingConditions, v.limit,
|
|
1888
|
+
const billingRaw = await boundedPart(http, "/V1.0/BillingItems/query", billingConditions, v.limit, OP3, billingIncludeFields);
|
|
1742
1889
|
let billingItems = billingRaw.items;
|
|
1743
1890
|
let billingNoteCharsTruncated = 0;
|
|
1744
1891
|
if (v.includeNotes) {
|
|
@@ -1782,7 +1929,7 @@ async function readTimePart(http, v, join) {
|
|
|
1782
1929
|
if (v.dateFrom !== void 0) timeConditions.push({ field: "dateWorked", op: "gte", value: v.dateFrom });
|
|
1783
1930
|
if (v.dateTo !== void 0) timeConditions.push({ field: "dateWorked", op: "lte", value: v.dateTo });
|
|
1784
1931
|
const timeFields = ["taskID", "ticketID", "hoursWorked", "dateWorked", "id", ...v.includeNotes ? ["summaryNotes"] : []];
|
|
1785
|
-
const raw = await boundedPart(http, "/V1.0/TimeEntries/query", timeConditions, v.limit,
|
|
1932
|
+
const raw = await boundedPart(http, "/V1.0/TimeEntries/query", timeConditions, v.limit, OP3, timeFields);
|
|
1786
1933
|
entries = raw.items;
|
|
1787
1934
|
let cappedItems = entries;
|
|
1788
1935
|
let noteCharsTruncated = 0;
|
|
@@ -2006,6 +2153,10 @@ var CompaniesResource = class extends BaseResource {
|
|
|
2006
2153
|
unbilledWork(id, opts) {
|
|
2007
2154
|
return unbilledWork(this, id, opts, this.http);
|
|
2008
2155
|
}
|
|
2156
|
+
/** Node-parity special op (hand-written in src/resources/special/companies.ts). */
|
|
2157
|
+
prepareCrmWrite(input) {
|
|
2158
|
+
return prepareCrmWrite(this, input, this.http);
|
|
2159
|
+
}
|
|
2009
2160
|
};
|
|
2010
2161
|
|
|
2011
2162
|
// src/resources/company_alerts.ts
|
|
@@ -4833,11 +4984,11 @@ var ContractServiceUnitsResource = class extends BaseResource {
|
|
|
4833
4984
|
};
|
|
4834
4985
|
|
|
4835
4986
|
// src/resources/special/contracts/lineOfBusiness.ts
|
|
4836
|
-
var
|
|
4987
|
+
var OP4 = "contracts.lineOfBusiness";
|
|
4837
4988
|
function positiveId(value, name) {
|
|
4838
4989
|
const n = typeof value === "string" && /^[0-9]+$/.test(value.trim()) ? Number(value) : value;
|
|
4839
4990
|
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
4840
|
-
throw new AutotaskConfigError(
|
|
4991
|
+
throw new AutotaskConfigError(OP4 + ": " + name + " must be a positive integer id (got " + String(value) + ")", { operation: OP4 });
|
|
4841
4992
|
}
|
|
4842
4993
|
return n;
|
|
4843
4994
|
}
|
|
@@ -4863,7 +5014,7 @@ async function lineOfBusiness(resource, contractIdIn, http, associationIdIn) {
|
|
|
4863
5014
|
markers: { association: "missing", organizationalLevel2: "missing", organizationalLevel1: "missing" }
|
|
4864
5015
|
};
|
|
4865
5016
|
}
|
|
4866
|
-
const chain = await resolveLineOfBusinessChain(http, associationId, { budget: 3, operation:
|
|
5017
|
+
const chain = await resolveLineOfBusinessChain(http, associationId, { budget: 3, operation: OP4, onError: "propagate" });
|
|
4867
5018
|
const { association, organizationalLevel2, organizationalLevel1, markers } = chain.lineOfBusiness;
|
|
4868
5019
|
return { contractId, associationSource, association, organizationalLevel2, organizationalLevel1, markers };
|
|
4869
5020
|
}
|
|
@@ -5337,7 +5488,7 @@ function appendWiresNote(wires, note) {
|
|
|
5337
5488
|
wires.note = (wires.note ? wires.note + " | " : "") + note;
|
|
5338
5489
|
}
|
|
5339
5490
|
function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
5340
|
-
const
|
|
5491
|
+
const OP12 = PROFITABILITY_OP;
|
|
5341
5492
|
const id = parseIdentifier(contractId, "contractId");
|
|
5342
5493
|
const idNum = typeof id === "number" ? id : Number(id);
|
|
5343
5494
|
const wires = { issued: 0, requests: 0, budget };
|
|
@@ -5349,19 +5500,19 @@ function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
|
5349
5500
|
} };
|
|
5350
5501
|
const readPart = (path, conditions, fields) => {
|
|
5351
5502
|
wires.issued += 1;
|
|
5352
|
-
return degradable(() => boundedPart(http, path, conditions, v.limit,
|
|
5503
|
+
return degradable(() => boundedPart(http, path, conditions, v.limit, OP12, fields));
|
|
5353
5504
|
};
|
|
5354
5505
|
const truncatedMonths = {};
|
|
5355
5506
|
const windowedPart = async (name, key, path, cond, fields, dateField) => {
|
|
5356
5507
|
const first = await readPart(path, cond, fields);
|
|
5357
5508
|
if (first.absent !== void 0 || !first.hasMore) return { part: stampPart(key, first, cond), rows: first.items };
|
|
5358
|
-
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit,
|
|
5509
|
+
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit, OP12, fields, dateField, v.dateFrom, v.dateTo, wires));
|
|
5359
5510
|
if (!("truncatedMonths" in wp)) return { part: stampPart(key, wp, cond), rows: [] };
|
|
5360
5511
|
const { truncatedMonths: tm, ...part } = wp;
|
|
5361
5512
|
truncatedMonths[name] = tm;
|
|
5362
5513
|
return { part, rows: wp.items };
|
|
5363
5514
|
};
|
|
5364
|
-
return { v, OP:
|
|
5515
|
+
return { v, OP: OP12, id, idNum, rawHttp, http, wires, windowOn, readPart, windowedPart, truncatedMonths };
|
|
5365
5516
|
}
|
|
5366
5517
|
|
|
5367
5518
|
// src/resources/special/contracts/profitability.stages.ts
|
|
@@ -10733,22 +10884,22 @@ function uiLinkFor(http, entity, id) {
|
|
|
10733
10884
|
}
|
|
10734
10885
|
|
|
10735
10886
|
// src/resources/special/ticket_charges.ts
|
|
10736
|
-
var
|
|
10887
|
+
var OP5 = "ticket_charges.moveToTicket";
|
|
10737
10888
|
var COPY = ["name", "description", "chargeType", "billingCodeID", "productID", "unitQuantity", "unitCost", "unitPrice", "datePurchased", "isBillableToCompany", "notes", "purchaseOrderNumber", "internalPurchaseOrderNumber"];
|
|
10738
10889
|
var CONTRACT_SCOPED = ["contractServiceID", "contractServiceBundleID"];
|
|
10739
10890
|
async function readCharge(http, id, retries) {
|
|
10740
|
-
const rows = await queryRows(http, "TicketCharges", [{ field: "id", op: "eq", value: id }],
|
|
10891
|
+
const rows = await queryRows(http, "TicketCharges", [{ field: "id", op: "eq", value: id }], OP5, retries);
|
|
10741
10892
|
return rows[0] ?? null;
|
|
10742
10893
|
}
|
|
10743
10894
|
async function checkSource({ http, source, sourceId, sourceTicketId, toTicketId }) {
|
|
10744
10895
|
if (source.isBilled === true) {
|
|
10745
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
10896
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP5}: ticket charge ${sourceId} is billed \u2014 move it in the Autotask UI`, { sourceId, toTicketId }, OP5, {
|
|
10746
10897
|
manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "tickets", sourceTicketId), reason: "billed charges are not moved by the API (the ticket link opens its charges)" }
|
|
10747
10898
|
});
|
|
10748
10899
|
}
|
|
10749
10900
|
}
|
|
10750
10901
|
var SPEC = {
|
|
10751
|
-
operation:
|
|
10902
|
+
operation: OP5,
|
|
10752
10903
|
entity: "ticket_charge",
|
|
10753
10904
|
readSource: readCharge,
|
|
10754
10905
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -11417,12 +11568,12 @@ async function slaHealthCheck(_resource, id, opts = {}, http) {
|
|
|
11417
11568
|
"tickets.slaHealthCheck"
|
|
11418
11569
|
);
|
|
11419
11570
|
const slas = rows.map((row) => {
|
|
11420
|
-
const
|
|
11571
|
+
const bool2 = (v) => typeof v === "boolean" ? v : null;
|
|
11421
11572
|
const num2 = (v) => typeof v === "number" ? v : null;
|
|
11422
11573
|
const milestones = [
|
|
11423
|
-
{ name: "firstResponse", met:
|
|
11424
|
-
{ name: "resolution", met:
|
|
11425
|
-
{ name: "resolutionPlan", met:
|
|
11574
|
+
{ name: "firstResponse", met: bool2(row.isFirstResponseMet), elapsedHours: num2(row.firstResponseElapsedHours) },
|
|
11575
|
+
{ name: "resolution", met: bool2(row.isResolutionMet), elapsedHours: num2(row.resolutionElapsedHours) },
|
|
11576
|
+
{ name: "resolutionPlan", met: bool2(row.isResolutionPlanMet), elapsedHours: num2(row.resolutionPlanElapsedHours) }
|
|
11426
11577
|
];
|
|
11427
11578
|
return { id: Number(row.id), name: row.serviceLevelAgreementName ?? null, milestones };
|
|
11428
11579
|
});
|
|
@@ -11440,11 +11591,11 @@ async function summary(_resource, id, opts = {}, http) {
|
|
|
11440
11591
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
11441
11592
|
const ageDays = typeof ticket.createDate === "string" && ticket.createDate !== "" ? Math.max(0, Math.floor((now.getTime() - Date.parse(ticket.createDate)) / MS)) : null;
|
|
11442
11593
|
const pick = (v) => typeof v === "number" ? v : null;
|
|
11443
|
-
const
|
|
11594
|
+
const str2 = (v) => typeof v === "string" && v !== "" ? v : null;
|
|
11444
11595
|
return {
|
|
11445
11596
|
ticketId,
|
|
11446
|
-
ticketNumber:
|
|
11447
|
-
title:
|
|
11597
|
+
ticketNumber: str2(ticket.ticketNumber),
|
|
11598
|
+
title: str2(ticket.title),
|
|
11448
11599
|
description: desc.value,
|
|
11449
11600
|
descriptionLength: desc.length,
|
|
11450
11601
|
descriptionTruncated: desc.truncated,
|
|
@@ -11454,8 +11605,8 @@ async function summary(_resource, id, opts = {}, http) {
|
|
|
11454
11605
|
contactID: pick(ticket.contactID),
|
|
11455
11606
|
assignedResourceID: pick(ticket.assignedResourceID),
|
|
11456
11607
|
projectID: pick(ticket.projectID),
|
|
11457
|
-
createDate:
|
|
11458
|
-
dueDateTime:
|
|
11608
|
+
createDate: str2(ticket.createDate),
|
|
11609
|
+
dueDateTime: str2(ticket.dueDateTime),
|
|
11459
11610
|
ageDays
|
|
11460
11611
|
};
|
|
11461
11612
|
}
|
|
@@ -12202,7 +12353,7 @@ async function fullContext(resource, id, opts = {}, http) {
|
|
|
12202
12353
|
}
|
|
12203
12354
|
|
|
12204
12355
|
// src/resources/special/tickets/forResource.ts
|
|
12205
|
-
var
|
|
12356
|
+
var OP6 = "tickets.forResource";
|
|
12206
12357
|
var TICKET_CHUNK = 100;
|
|
12207
12358
|
var LIMIT_DEFAULT = 100;
|
|
12208
12359
|
var LIMIT_MAX = 500;
|
|
@@ -12211,7 +12362,7 @@ var TICKET_FIELDS2 = ["id", "ticketNumber", "title", "status", "assignedResource
|
|
|
12211
12362
|
function positiveId2(value, name) {
|
|
12212
12363
|
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
12213
12364
|
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
12214
|
-
throw new AutotaskConfigError(`${
|
|
12365
|
+
throw new AutotaskConfigError(`${OP6}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP6 });
|
|
12215
12366
|
}
|
|
12216
12367
|
return n;
|
|
12217
12368
|
}
|
|
@@ -12221,28 +12372,28 @@ async function ticketPage(http, filter, maxRecords, fields) {
|
|
|
12221
12372
|
idempotent: true,
|
|
12222
12373
|
path: "/V1.0/Tickets/query",
|
|
12223
12374
|
body: { MaxRecords: maxRecords, filter, IncludeFields: fields },
|
|
12224
|
-
operation:
|
|
12375
|
+
operation: OP6
|
|
12225
12376
|
});
|
|
12226
|
-
const page = unwrapItems(res,
|
|
12377
|
+
const page = unwrapItems(res, OP6);
|
|
12227
12378
|
return { rows: page.items, more: page.pageDetails.nextPageUrl !== null };
|
|
12228
12379
|
}
|
|
12229
12380
|
async function forResource(_resource, resourceIDIn, http, opts = {}) {
|
|
12230
|
-
if (opts === null || typeof opts !== "object") throw new AutotaskConfigError(`${
|
|
12381
|
+
if (opts === null || typeof opts !== "object") throw new AutotaskConfigError(`${OP6}: opts must be an object`, { operation: OP6 });
|
|
12231
12382
|
const resourceID = positiveId2(resourceIDIn, "resourceID");
|
|
12232
12383
|
const scope = opts.scope ?? "both";
|
|
12233
12384
|
if (scope !== "primary" && scope !== "secondary" && scope !== "both") {
|
|
12234
|
-
throw new AutotaskConfigError(`${
|
|
12385
|
+
throw new AutotaskConfigError(`${OP6}: scope must be 'primary', 'secondary' or 'both' (got ${JSON.stringify(opts.scope)})`, { operation: OP6 });
|
|
12235
12386
|
}
|
|
12236
12387
|
if (opts.openOnly !== void 0 && typeof opts.openOnly !== "boolean") {
|
|
12237
|
-
throw new AutotaskConfigError(`${
|
|
12388
|
+
throw new AutotaskConfigError(`${OP6}: openOnly must be a boolean`, { operation: OP6 });
|
|
12238
12389
|
}
|
|
12239
12390
|
const limit = opts.limit === void 0 ? LIMIT_DEFAULT : opts.limit;
|
|
12240
12391
|
if (!Number.isInteger(limit) || limit < 1 || limit > LIMIT_MAX) {
|
|
12241
|
-
throw new AutotaskConfigError(`${
|
|
12392
|
+
throw new AutotaskConfigError(`${OP6}: limit must be an integer 1-${LIMIT_MAX} (got ${String(opts.limit)})`, { operation: OP6 });
|
|
12242
12393
|
}
|
|
12243
12394
|
const includeFields = opts.includeFields ?? [];
|
|
12244
12395
|
if (!Array.isArray(includeFields) || includeFields.some((f) => typeof f !== "string" || f.length === 0)) {
|
|
12245
|
-
throw new AutotaskConfigError(`${
|
|
12396
|
+
throw new AutotaskConfigError(`${OP6}: includeFields must be a list of field names`, { operation: OP6 });
|
|
12246
12397
|
}
|
|
12247
12398
|
const openOnly = opts.openOnly === true;
|
|
12248
12399
|
const fields = [.../* @__PURE__ */ new Set([...TICKET_FIELDS2, ...includeFields])];
|
|
@@ -12257,9 +12408,9 @@ async function forResource(_resource, resourceIDIn, http, opts = {}) {
|
|
|
12257
12408
|
idempotent: true,
|
|
12258
12409
|
path: "/V1.0/TicketSecondaryResources/query",
|
|
12259
12410
|
body: { MaxRecords: LINK_READ_CAP, filter: [{ field: "resourceID", op: "eq", value: resourceID }], IncludeFields: ["ticketID"] },
|
|
12260
|
-
operation:
|
|
12411
|
+
operation: OP6
|
|
12261
12412
|
});
|
|
12262
|
-
const page = unwrapItems(res,
|
|
12413
|
+
const page = unwrapItems(res, OP6);
|
|
12263
12414
|
for (const row of page.items) if (typeof row.ticketID === "number" && !linkIds.includes(row.ticketID)) linkIds.push(row.ticketID);
|
|
12264
12415
|
if (page.items.length >= LINK_READ_CAP || page.pageDetails.nextPageUrl !== null) capped = true;
|
|
12265
12416
|
}
|
|
@@ -12312,11 +12463,11 @@ async function forResource(_resource, resourceIDIn, http, opts = {}) {
|
|
|
12312
12463
|
}
|
|
12313
12464
|
|
|
12314
12465
|
// src/resources/special/tickets/lineOfBusiness.ts
|
|
12315
|
-
var
|
|
12466
|
+
var OP7 = "tickets.lineOfBusiness";
|
|
12316
12467
|
function positiveId3(value, name) {
|
|
12317
12468
|
const n = typeof value === "string" && /^[0-9]+$/.test(value.trim()) ? Number(value) : value;
|
|
12318
12469
|
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
12319
|
-
throw new AutotaskConfigError(
|
|
12470
|
+
throw new AutotaskConfigError(OP7 + ": " + name + " must be a positive integer id (got " + String(value) + ")", { operation: OP7 });
|
|
12320
12471
|
}
|
|
12321
12472
|
return n;
|
|
12322
12473
|
}
|
|
@@ -12339,7 +12490,7 @@ async function lineOfBusiness2(resource, ticketIdIn, http, associationIdIn) {
|
|
|
12339
12490
|
if (contractRef !== void 0) {
|
|
12340
12491
|
const client = http.client;
|
|
12341
12492
|
if (client === void 0) {
|
|
12342
|
-
throw new AutotaskConfigError(
|
|
12493
|
+
throw new AutotaskConfigError(OP7 + ": the transport is not wired to an AutotaskClient (the composite must run through the client)", { operation: OP7 });
|
|
12343
12494
|
}
|
|
12344
12495
|
try {
|
|
12345
12496
|
const contract = await client.contracts.get(contractRef);
|
|
@@ -12370,7 +12521,7 @@ async function lineOfBusiness2(resource, ticketIdIn, http, associationIdIn) {
|
|
|
12370
12521
|
markers: { association: contractUnread ? "unresolved" : "missing", organizationalLevel2: "missing", organizationalLevel1: "missing" }
|
|
12371
12522
|
};
|
|
12372
12523
|
}
|
|
12373
|
-
const chain = await resolveLineOfBusinessChain(http, associationId, { budget: 3, operation:
|
|
12524
|
+
const chain = await resolveLineOfBusinessChain(http, associationId, { budget: 3, operation: OP7, onError: "propagate" });
|
|
12374
12525
|
const { association, organizationalLevel2, organizationalLevel1, markers } = chain.lineOfBusiness;
|
|
12375
12526
|
return { ticketId, associationSource, contractId, association, organizationalLevel2, organizationalLevel1, markers };
|
|
12376
12527
|
}
|
|
@@ -12575,6 +12726,33 @@ var TicketWebhooksResource = class extends BaseResource {
|
|
|
12575
12726
|
}
|
|
12576
12727
|
};
|
|
12577
12728
|
|
|
12729
|
+
// src/resources/special/time_entries/posted.ts
|
|
12730
|
+
var isApproved = (entry) => typeof entry.billingApprovalDateTime === "string" && entry.billingApprovalDateTime.trim() !== "";
|
|
12731
|
+
async function postedStateOf(http, entries, op) {
|
|
12732
|
+
const states = /* @__PURE__ */ new Map();
|
|
12733
|
+
const ids2 = [...new Set(entries.map((e) => e.id))];
|
|
12734
|
+
let incomplete = false;
|
|
12735
|
+
for (let c = 0; c < ids2.length; c += FETCH_BY_IDS_CHUNK_SIZE) {
|
|
12736
|
+
const chunk = ids2.slice(c, c + FETCH_BY_IDS_CHUNK_SIZE);
|
|
12737
|
+
const res = await http.request({
|
|
12738
|
+
method: "POST",
|
|
12739
|
+
idempotent: true,
|
|
12740
|
+
path: "/V1.0/BillingItems/query",
|
|
12741
|
+
body: { MaxRecords: 500, IncludeFields: ["id", "timeEntryID"], filter: [{ field: "timeEntryID", op: "in", value: chunk }] },
|
|
12742
|
+
operation: op
|
|
12743
|
+
});
|
|
12744
|
+
const { items, pageDetails } = unwrapItems(res);
|
|
12745
|
+
const billed = new Set(items.map((i) => i.timeEntryID));
|
|
12746
|
+
const more = pageDetails.nextPageUrl != null;
|
|
12747
|
+
if (more) incomplete = true;
|
|
12748
|
+
for (const id of chunk) {
|
|
12749
|
+
if (billed.has(id)) states.set(id, "posted");
|
|
12750
|
+
else if (!more) states.set(id, "unposted");
|
|
12751
|
+
}
|
|
12752
|
+
}
|
|
12753
|
+
return { states, incomplete };
|
|
12754
|
+
}
|
|
12755
|
+
|
|
12578
12756
|
// src/resources/special/time_entries/windows.ts
|
|
12579
12757
|
var DAY_MS2 = 864e5;
|
|
12580
12758
|
var iso2 = (ms) => new Date(ms).toISOString().slice(0, 19) + "Z";
|
|
@@ -12615,48 +12793,56 @@ async function customRange3(_resource, from, to, http, opts) {
|
|
|
12615
12793
|
const { items } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), "time_entries.customRange", http);
|
|
12616
12794
|
return items;
|
|
12617
12795
|
}
|
|
12618
|
-
async function
|
|
12619
|
-
const filter = [
|
|
12620
|
-
{ field: "postedDate", op: mode === "posted" ? "exist" : "notExist" }
|
|
12621
|
-
];
|
|
12622
|
-
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
12796
|
+
async function postedStateQuery(_resource, mode, http, opts) {
|
|
12623
12797
|
const operation = mode === "posted" ? "time_entries.getPosted" : "time_entries.getUnposted";
|
|
12624
|
-
const
|
|
12625
|
-
|
|
12798
|
+
const window = { from: null, to: null };
|
|
12799
|
+
const filter = [];
|
|
12800
|
+
const fromB = opts?.dateFrom === void 0 ? void 0 : customRangeDay(opts.dateFrom, "from", void 0, operation);
|
|
12801
|
+
const toB = opts?.dateTo === void 0 ? void 0 : customRangeDay(opts.dateTo, "to", void 0, operation);
|
|
12802
|
+
if (fromB && toB && fromB.ms > toB.ms) {
|
|
12803
|
+
throw new AutotaskConfigError(`${operation}: dateFrom (${iso2(fromB.ms)}) must not be after dateTo (${iso2(toB.ms)})`, { operation });
|
|
12804
|
+
}
|
|
12805
|
+
if (fromB) filter.push({ field: "dateWorked", op: "gte", value: window.from = iso2(fromB.ms) });
|
|
12806
|
+
if (toB) filter.push({ field: "dateWorked", op: "lte", value: window.to = iso2(toB.ms + DAY_MS2 - 1) });
|
|
12807
|
+
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
12808
|
+
const read = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), operation, http);
|
|
12809
|
+
const { states, incomplete } = await postedStateOf(http, read.items, operation);
|
|
12810
|
+
const items = read.items.filter((e) => states.get(e.id) === mode);
|
|
12811
|
+
const approvedCount = items.filter(isApproved).length;
|
|
12812
|
+
return { items, hasMore: read.hasMore || incomplete, window, approvedCount, unapprovedCount: items.length - approvedCount };
|
|
12626
12813
|
}
|
|
12627
12814
|
async function postedWithin(_resource, from, to, http, opts, opLabel = "time_entries.postedWithin") {
|
|
12628
|
-
const
|
|
12629
|
-
if (opts?.tz !== void 0) validateZone(opts.tz,
|
|
12630
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
12631
|
-
const toB = customRangeDay(to, "to", void 0,
|
|
12815
|
+
const OP12 = opLabel;
|
|
12816
|
+
if (opts?.tz !== void 0) validateZone(opts.tz, OP12);
|
|
12817
|
+
const fromB = customRangeDay(from, "from", void 0, OP12);
|
|
12818
|
+
const toB = customRangeDay(to, "to", void 0, OP12);
|
|
12632
12819
|
if (fromB.ms > toB.ms) {
|
|
12633
|
-
throw new AutotaskConfigError(`${
|
|
12820
|
+
throw new AutotaskConfigError(`${OP12}: from (${iso2(fromB.ms)}) must not be after to (${iso2(toB.ms)})`, { operation: OP12 });
|
|
12634
12821
|
}
|
|
12635
12822
|
const windowToMs = toB.ms + DAY_MS2 - 1;
|
|
12636
12823
|
const filter = [
|
|
12637
12824
|
{ field: "dateWorked", op: "gte", value: iso2(fromB.ms) },
|
|
12638
12825
|
{ field: "dateWorked", op: "lte", value: iso2(windowToMs) }
|
|
12639
12826
|
];
|
|
12640
|
-
if (opts?.posted === true) filter.push({ field: "postedDate", op: "exist" });
|
|
12641
|
-
else if (opts?.posted === false) filter.push({ field: "postedDate", op: "notExist" });
|
|
12642
12827
|
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
12643
12828
|
if (opts?.resourceID !== void 0) filter.push({ field: "resourceID", op: "eq", value: opts.resourceID });
|
|
12644
|
-
const { items, hasMore } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit),
|
|
12829
|
+
const { items, hasMore } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), OP12, http);
|
|
12830
|
+
const { states, incomplete } = await postedStateOf(http, items, OP12);
|
|
12645
12831
|
let postedCount = 0;
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
if (
|
|
12649
|
-
|
|
12650
|
-
}
|
|
12832
|
+
let unpostedCount = 0;
|
|
12833
|
+
for (const state of states.values()) {
|
|
12834
|
+
if (state === "posted") postedCount += 1;
|
|
12835
|
+
else unpostedCount += 1;
|
|
12651
12836
|
}
|
|
12837
|
+
const keep = opts?.posted === void 0 ? void 0 : opts.posted ? "posted" : "unposted";
|
|
12652
12838
|
return {
|
|
12653
|
-
items,
|
|
12839
|
+
items: keep === void 0 ? items : items.filter((e) => states.get(e.id) === keep),
|
|
12654
12840
|
postedCount,
|
|
12655
|
-
unpostedCount
|
|
12841
|
+
unpostedCount,
|
|
12656
12842
|
window: { from: iso2(fromB.ms), to: iso2(windowToMs) },
|
|
12657
12843
|
// A live next-page cursor past the bounded read: the counts above cover
|
|
12658
12844
|
// the returned rows ONLY when this is true (issue #66 AC1 — the 500-row cap case is the general case).
|
|
12659
|
-
truncated: hasMore
|
|
12845
|
+
truncated: hasMore || incomplete
|
|
12660
12846
|
};
|
|
12661
12847
|
}
|
|
12662
12848
|
async function dateWorkedWindow(resource, from, to, http, opts) {
|
|
@@ -12851,7 +13037,7 @@ async function resolveCode(http, family, familyLabel2, pinnedId, departments, op
|
|
|
12851
13037
|
}
|
|
12852
13038
|
|
|
12853
13039
|
// src/resources/special/time_entries/createForResource.ts
|
|
12854
|
-
var
|
|
13040
|
+
var OP8 = "time_entries.createForResource";
|
|
12855
13041
|
var PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
12856
13042
|
"internallAllocationCodeID",
|
|
12857
13043
|
"ticketID",
|
|
@@ -12887,30 +13073,30 @@ function validateCreateParams(params) {
|
|
|
12887
13073
|
if (pinCount > 1) {
|
|
12888
13074
|
throw new AutotaskConfigError(
|
|
12889
13075
|
"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)",
|
|
12890
|
-
{ operation:
|
|
13076
|
+
{ operation: OP8 }
|
|
12891
13077
|
);
|
|
12892
13078
|
}
|
|
12893
13079
|
if (pins.role !== void 0 && pins.code !== void 0) {
|
|
12894
13080
|
throw new AutotaskConfigError(
|
|
12895
13081
|
"createForResource: a pinned roleID is only meaningful with the task/ticket linkage (internallAllocationCodeID pins the internal linkage and owns the code) \u2014 drop one",
|
|
12896
|
-
{ operation:
|
|
13082
|
+
{ operation: OP8 }
|
|
12897
13083
|
);
|
|
12898
13084
|
}
|
|
12899
13085
|
if (params.hoursWorked !== void 0 && (typeof params.hoursWorked !== "number" || !Number.isFinite(params.hoursWorked) || params.hoursWorked <= 0)) {
|
|
12900
13086
|
throw new AutotaskConfigError(
|
|
12901
13087
|
`createForResource: hoursWorked must be a positive finite number (got ${String(params.hoursWorked)})`,
|
|
12902
|
-
{ operation:
|
|
13088
|
+
{ operation: OP8 }
|
|
12903
13089
|
);
|
|
12904
13090
|
}
|
|
12905
13091
|
return pins;
|
|
12906
13092
|
}
|
|
12907
13093
|
async function assertResourceActive(http, rid) {
|
|
12908
|
-
const rec = await fetchItem2(http, `/V1.0/Resources/${rid}`,
|
|
13094
|
+
const rec = await fetchItem2(http, `/V1.0/Resources/${rid}`, OP8);
|
|
12909
13095
|
if (rec === null) {
|
|
12910
13096
|
throw new ResourceInvalidError(`Resource ${rid} was not found \u2014 a time entry cannot attach to a missing resource`, {
|
|
12911
13097
|
resourceIds: [rid],
|
|
12912
13098
|
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
12913
|
-
operation:
|
|
13099
|
+
operation: OP8,
|
|
12914
13100
|
category: "not_found",
|
|
12915
13101
|
suggestedAction: "Resolve the resource id (resources.get / resources.search) and retry."
|
|
12916
13102
|
});
|
|
@@ -12919,7 +13105,7 @@ async function assertResourceActive(http, rid) {
|
|
|
12919
13105
|
throw new ResourceInvalidError(`Resource ${rid} is not active (isActive !== true) \u2014 the vendor would reject a time entry for an inactive resource`, {
|
|
12920
13106
|
resourceIds: [rid],
|
|
12921
13107
|
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
12922
|
-
operation:
|
|
13108
|
+
operation: OP8,
|
|
12923
13109
|
suggestedAction: "Activate the resource or pick an active one."
|
|
12924
13110
|
});
|
|
12925
13111
|
}
|
|
@@ -12929,11 +13115,11 @@ async function resolveAllocationCode(http, rid, linkage, pins, departments) {
|
|
|
12929
13115
|
const pinnedForCode = pins.code !== void 0 ? pins.code : pins.role;
|
|
12930
13116
|
let code = null;
|
|
12931
13117
|
const familiesTried = [];
|
|
12932
|
-
code = await resolveCode(http, familyOf(linkage), familyLabel(familyOf(linkage)), pinnedForCode, departments,
|
|
13118
|
+
code = await resolveCode(http, familyOf(linkage), familyLabel(familyOf(linkage)), pinnedForCode, departments, OP8);
|
|
12933
13119
|
familiesTried.push({ family: familyOf(linkage), label: familyLabel(familyOf(linkage)), filters: [{ field: "useType", op: "eq", value: familyOf(linkage) }, { field: "isActive", op: "eq", value: true }] });
|
|
12934
13120
|
if (code === null && linkage === "task" && pins.role === void 0) {
|
|
12935
13121
|
usedLinkage = "internal";
|
|
12936
|
-
code = await resolveCode(http, USE_TYPE_INTERNAL, familyLabel(USE_TYPE_INTERNAL), void 0, departments,
|
|
13122
|
+
code = await resolveCode(http, USE_TYPE_INTERNAL, familyLabel(USE_TYPE_INTERNAL), void 0, departments, OP8);
|
|
12937
13123
|
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 }] });
|
|
12938
13124
|
}
|
|
12939
13125
|
if (code === null) {
|
|
@@ -12943,7 +13129,7 @@ async function resolveAllocationCode(http, rid, linkage, pins, departments) {
|
|
|
12943
13129
|
families: familiesTried,
|
|
12944
13130
|
resourceDepartments: [...departments].sort((a, b) => a - b)
|
|
12945
13131
|
},
|
|
12946
|
-
{ operation:
|
|
13132
|
+
{ operation: OP8 }
|
|
12947
13133
|
);
|
|
12948
13134
|
}
|
|
12949
13135
|
const resolvedCode = codeInfo(code);
|
|
@@ -12982,7 +13168,7 @@ function createDryRun2(http, rid, body, linkage, usedLinkage, resolvedCode) {
|
|
|
12982
13168
|
}
|
|
12983
13169
|
return buildDryRunResult(
|
|
12984
13170
|
{
|
|
12985
|
-
operation:
|
|
13171
|
+
operation: OP8,
|
|
12986
13172
|
method: "POST",
|
|
12987
13173
|
path: "/V1.0/TimeEntries",
|
|
12988
13174
|
ids: [rid],
|
|
@@ -13024,7 +13210,7 @@ async function createWithAttempts(resource, http, opts, body, defaults, pins, us
|
|
|
13024
13210
|
const gate3 = ROLE_REJECTION_PATTERN.test(text);
|
|
13025
13211
|
if (usedLinkage !== "internal" && gate3) {
|
|
13026
13212
|
if (!taskRoleFetched && usedLinkage === "task" && pins.task !== void 0) {
|
|
13027
|
-
const task = await fetchItem2(http, `/V1.0/Tasks/${pins.task}`,
|
|
13213
|
+
const task = await fetchItem2(http, `/V1.0/Tasks/${pins.task}`, OP8);
|
|
13028
13214
|
taskRoleFetched = true;
|
|
13029
13215
|
if (task !== null) pushLadder(task.assignedResourceRoleID);
|
|
13030
13216
|
}
|
|
@@ -13050,7 +13236,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
|
|
|
13050
13236
|
const pins = validateCreateParams(params);
|
|
13051
13237
|
await assertResourceActive(http, rid);
|
|
13052
13238
|
const linkage = pins.code !== void 0 ? "internal" : pins.ticket !== void 0 ? "ticket" : pins.task !== void 0 ? "task" : "internal";
|
|
13053
|
-
const roleRows = await resourceRoleRows(http, rid,
|
|
13239
|
+
const roleRows = await resourceRoleRows(http, rid, OP8);
|
|
13054
13240
|
const departments = roleRows.departments;
|
|
13055
13241
|
const { usedLinkage, resolvedCode } = await resolveAllocationCode(http, rid, linkage, pins, departments);
|
|
13056
13242
|
const defaults = windowDefaults(params);
|
|
@@ -13073,12 +13259,12 @@ function discoveryField(err, body, defaults) {
|
|
|
13073
13259
|
}
|
|
13074
13260
|
|
|
13075
13261
|
// src/resources/special/time_entries/move.ts
|
|
13076
|
-
var
|
|
13262
|
+
var OP9 = "time_entries.moveToTicket";
|
|
13077
13263
|
var COPY2 = ["resourceID", "roleID", "billingCodeID", "dateWorked", "startDateTime", "endDateTime", "hoursWorked", "offsetHours", "summaryNotes", "internalNotes", "isNonBillable", "showOnInvoice"];
|
|
13078
13264
|
var CONTRACT_SCOPED2 = ["contractID", "contractServiceID", "contractServiceBundleID"];
|
|
13079
13265
|
async function readTimeEntry(http, id, retries) {
|
|
13080
13266
|
try {
|
|
13081
|
-
const res = await http.request({ method: "GET", path: `/V1.0/TimeEntries/${id}`, operation:
|
|
13267
|
+
const res = await http.request({ method: "GET", path: `/V1.0/TimeEntries/${id}`, operation: OP9, maxRetries: retries });
|
|
13082
13268
|
return res?.item ?? null;
|
|
13083
13269
|
} catch (e) {
|
|
13084
13270
|
if (isAutotaskError(e) && (e.httpStatus === 404 || e.code === "NOT_FOUND")) return null;
|
|
@@ -13088,27 +13274,27 @@ async function readTimeEntry(http, id, retries) {
|
|
|
13088
13274
|
async function checkSource2({ http, source, sourceId, toTicketId, retries }) {
|
|
13089
13275
|
const ids2 = { sourceId, toTicketId };
|
|
13090
13276
|
if (source.taskID !== null && source.taskID !== void 0) {
|
|
13091
|
-
throw refuse("MOVE_SOURCE_UNSUPPORTED", `${
|
|
13277
|
+
throw refuse("MOVE_SOURCE_UNSUPPORTED", `${OP9}: time entry ${sourceId} is on a task \u2014 only ticket time entries are moved`, ids2, OP9);
|
|
13092
13278
|
}
|
|
13093
13279
|
if (source.billingApprovalDateTime || source.billingApprovalResourceID || source.billingApprovalLevelMostRecent) {
|
|
13094
|
-
throw refuse("MOVE_BLOCKED_APPROVED", `${
|
|
13280
|
+
throw refuse("MOVE_BLOCKED_APPROVED", `${OP9}: time entry ${sourceId} has billing approval set \u2014 move it in the Autotask UI`, ids2, OP9, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "approved entries are not moved by the API" } });
|
|
13095
13281
|
}
|
|
13096
13282
|
let billing;
|
|
13097
13283
|
try {
|
|
13098
|
-
billing = await queryRows(http, "BillingItems", [{ field: "timeEntryID", op: "eq", value: sourceId }],
|
|
13284
|
+
billing = await queryRows(http, "BillingItems", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP9, retries);
|
|
13099
13285
|
} catch {
|
|
13100
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
13286
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP9}: the billing items of time entry ${sourceId} could not be read \u2014 refused (fails closed)`, ids2, OP9, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billing state unreadable" } });
|
|
13101
13287
|
}
|
|
13102
13288
|
if (billing.some((b) => b.invoiceID || b.postedDate)) {
|
|
13103
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
13289
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP9}: time entry ${sourceId} is billed or posted \u2014 move it in the Autotask UI`, ids2, OP9, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billed entries are not moved by the API" } });
|
|
13104
13290
|
}
|
|
13105
|
-
const attachments = await queryRows(http, "TimeEntryAttachments", [{ field: "timeEntryID", op: "eq", value: sourceId }],
|
|
13291
|
+
const attachments = await queryRows(http, "TimeEntryAttachments", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP9, retries);
|
|
13106
13292
|
if (attachments.length > 0) {
|
|
13107
|
-
throw refuse("MOVE_BLOCKED_ATTACHMENTS", `${
|
|
13293
|
+
throw refuse("MOVE_BLOCKED_ATTACHMENTS", `${OP9}: time entry ${sourceId} has attachments, which a delete would destroy \u2014 move it in the Autotask UI`, ids2, OP9, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "entries with attachments are not moved by the API" } });
|
|
13108
13294
|
}
|
|
13109
13295
|
}
|
|
13110
13296
|
var SPEC2 = {
|
|
13111
|
-
operation:
|
|
13297
|
+
operation: OP9,
|
|
13112
13298
|
entity: "time_entry",
|
|
13113
13299
|
readSource: readTimeEntry,
|
|
13114
13300
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -13143,7 +13329,7 @@ function moveToTicket2(_resource, sourceId, toTicketId, opts, http) {
|
|
|
13143
13329
|
}
|
|
13144
13330
|
|
|
13145
13331
|
// src/resources/special/time_entries/approve.ts
|
|
13146
|
-
var
|
|
13332
|
+
var OP10 = "time_entries.approve";
|
|
13147
13333
|
var APPROVAL_LEVELS_PATH = "/V1.0/BillingItemApprovalLevels";
|
|
13148
13334
|
function toIso(value) {
|
|
13149
13335
|
if (value instanceof Date) {
|
|
@@ -13181,7 +13367,7 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
13181
13367
|
if (opts.dryRun === true) {
|
|
13182
13368
|
return buildDryRunResult(
|
|
13183
13369
|
{
|
|
13184
|
-
operation:
|
|
13370
|
+
operation: OP10,
|
|
13185
13371
|
method: "POST",
|
|
13186
13372
|
path: APPROVAL_LEVELS_PATH,
|
|
13187
13373
|
ids: [numericId],
|
|
@@ -13204,10 +13390,10 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
13204
13390
|
method: "POST",
|
|
13205
13391
|
path: APPROVAL_LEVELS_PATH,
|
|
13206
13392
|
body,
|
|
13207
|
-
operation:
|
|
13393
|
+
operation: OP10,
|
|
13208
13394
|
maxRetries: opts.maxRetries
|
|
13209
13395
|
});
|
|
13210
|
-
const levelId = toNumber(parseIdentifier(unwrapOperation(created,
|
|
13396
|
+
const levelId = toNumber(parseIdentifier(unwrapOperation(created, OP10).id, "billingItemApprovalLevelID"));
|
|
13211
13397
|
await resource.update(
|
|
13212
13398
|
numericId,
|
|
13213
13399
|
{ billingApprovalDateTime: approvalDateTime, billingApprovalResourceID: approvalResourceID },
|
|
@@ -13230,28 +13416,28 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
13230
13416
|
}
|
|
13231
13417
|
|
|
13232
13418
|
// src/resources/special/time_entries/recentTickets.ts
|
|
13233
|
-
var
|
|
13419
|
+
var OP11 = "time_entries.recentTickets";
|
|
13234
13420
|
var DAY_MS3 = 864e5;
|
|
13235
13421
|
var TICKET_CHUNK2 = 100;
|
|
13236
13422
|
var TICKET_FIELDS3 = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
13237
13423
|
function positiveId4(value, name) {
|
|
13238
13424
|
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
13239
13425
|
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
13240
|
-
throw new AutotaskConfigError(`${
|
|
13426
|
+
throw new AutotaskConfigError(`${OP11}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP11 });
|
|
13241
13427
|
}
|
|
13242
13428
|
return n;
|
|
13243
13429
|
}
|
|
13244
13430
|
async function recentTickets(_resource, from, to, http, opts) {
|
|
13245
13431
|
if (opts === void 0 || opts === null || typeof opts !== "object") {
|
|
13246
|
-
throw new AutotaskConfigError(`${
|
|
13432
|
+
throw new AutotaskConfigError(`${OP11}: opts { resourceID, companyID?, tz?, limit? } is required`, { operation: OP11 });
|
|
13247
13433
|
}
|
|
13248
13434
|
const resourceID = positiveId4(opts.resourceID, "resourceID");
|
|
13249
13435
|
const companyID = opts.companyID === void 0 ? void 0 : positiveId4(opts.companyID, "companyID");
|
|
13250
|
-
if (opts.tz !== void 0) validateZone(opts.tz,
|
|
13251
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
13252
|
-
const toB = customRangeDay(to, "to", void 0,
|
|
13436
|
+
if (opts.tz !== void 0) validateZone(opts.tz, OP11);
|
|
13437
|
+
const fromB = customRangeDay(from, "from", void 0, OP11);
|
|
13438
|
+
const toB = customRangeDay(to, "to", void 0, OP11);
|
|
13253
13439
|
if (fromB.ms > toB.ms) {
|
|
13254
|
-
throw new AutotaskConfigError(`${
|
|
13440
|
+
throw new AutotaskConfigError(`${OP11}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`, { operation: OP11 });
|
|
13255
13441
|
}
|
|
13256
13442
|
const windowToMs = toB.ms + DAY_MS3 - 1;
|
|
13257
13443
|
const { items: entries, hasMore } = await queryTimeEntries(
|
|
@@ -13262,7 +13448,7 @@ async function recentTickets(_resource, from, to, http, opts) {
|
|
|
13262
13448
|
{ field: "resourceID", op: "eq", value: resourceID }
|
|
13263
13449
|
],
|
|
13264
13450
|
clampWindowLimit(opts.limit),
|
|
13265
|
-
|
|
13451
|
+
OP11,
|
|
13266
13452
|
http
|
|
13267
13453
|
);
|
|
13268
13454
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -13288,9 +13474,9 @@ async function recentTickets(_resource, from, to, http, opts) {
|
|
|
13288
13474
|
idempotent: true,
|
|
13289
13475
|
path: "/V1.0/Tickets/query",
|
|
13290
13476
|
body: { MaxRecords: TICKET_CHUNK2, filter, IncludeFields: TICKET_FIELDS3 },
|
|
13291
|
-
operation:
|
|
13477
|
+
operation: OP11
|
|
13292
13478
|
});
|
|
13293
|
-
for (const t of unwrapItems(res,
|
|
13479
|
+
for (const t of unwrapItems(res, OP11).items) if (typeof t.id === "number") tickets.set(t.id, t);
|
|
13294
13480
|
}
|
|
13295
13481
|
const items = [];
|
|
13296
13482
|
const unresolvedTicketIDs = [];
|
|
@@ -13411,11 +13597,11 @@ var TimeEntriesResource = class extends BaseResource {
|
|
|
13411
13597
|
}
|
|
13412
13598
|
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
|
|
13413
13599
|
getPosted(opts) {
|
|
13414
|
-
return
|
|
13600
|
+
return postedStateQuery(this, "posted", this.http, opts);
|
|
13415
13601
|
}
|
|
13416
13602
|
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
|
|
13417
13603
|
getUnposted(opts) {
|
|
13418
|
-
return
|
|
13604
|
+
return postedStateQuery(this, "unposted", this.http, opts);
|
|
13419
13605
|
}
|
|
13420
13606
|
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
|
|
13421
13607
|
last14(opts) {
|