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.
Files changed (38) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/MCP_TOOL_CATALOG.json +31 -12
  3. package/MCP_TOOL_MANIFEST.md +18 -19
  4. package/MCP_TOOL_OVERRIDES.json +0 -12
  5. package/N8N_MCP_MANIFEST.json +44 -18
  6. package/N8N_MCP_MANIFEST.md +15 -14
  7. package/README.md +8 -6
  8. package/capabilities.json +1579 -1414
  9. package/dist/capabilities.cjs +2 -2
  10. package/dist/capabilities.js +1 -1
  11. package/dist/{chunk-3H3RJ4LA.cjs → chunk-3LAZZT7V.cjs} +143 -143
  12. package/dist/{chunk-PCLL536B.cjs → chunk-4QN43TRP.cjs} +75 -50
  13. package/dist/{chunk-6UA544OR.js → chunk-5DS6F6GF.js} +299 -113
  14. package/dist/{chunk-RQZUV74N.js → chunk-7WNE5MPG.js} +2 -2
  15. package/dist/{chunk-INSFDXHP.js → chunk-MFDQHFGJ.js} +28 -3
  16. package/dist/{chunk-DQLEVKZS.cjs → chunk-OO67MRHN.cjs} +814 -628
  17. package/dist/{chunk-QXUN2VCJ.cjs → chunk-SP2SJJZX.cjs} +1578 -1413
  18. package/dist/{chunk-EPOHRNSX.js → chunk-VKE56TTN.js} +1578 -1413
  19. package/dist/{index-Uqj987EE.d.ts → index-BaeMHZPk.d.ts} +149 -12
  20. package/dist/{index--5jrRl4u.d.cts → index-Cq0VsfSz.d.cts} +149 -12
  21. package/dist/index.cjs +22 -22
  22. package/dist/index.d.cts +2 -2
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +4 -4
  25. package/dist/operations/index.cjs +120 -133
  26. package/dist/operations/index.d.cts +2 -2
  27. package/dist/operations/index.d.ts +2 -2
  28. package/dist/operations/index.js +3 -16
  29. package/dist/pool.cjs +9 -9
  30. package/dist/pool.d.cts +1 -1
  31. package/dist/pool.d.ts +1 -1
  32. package/dist/pool.js +4 -4
  33. package/dist/resources/index.cjs +4 -4
  34. package/dist/resources/index.d.cts +1 -1
  35. package/dist/resources/index.d.ts +1 -1
  36. package/dist/resources/index.js +3 -3
  37. package/docs/API.md +62 -37
  38. 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-INSFDXHP.js";
44
+ } from "./chunk-MFDQHFGJ.js";
44
45
  import {
45
46
  CAPABILITIES
46
- } from "./chunk-EPOHRNSX.js";
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 candidate = Date.UTC(y, m - 1, d);
277
- const offset = tzOffsetMs(tz, candidate);
278
- let start = candidate - offset;
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 = candidate - tzOffsetMs(tz, 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 } = dateWindowBounds(kind, now, zone);
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
- const fromB = customRangeDay(from, "from", zone, operation);
488
- const toB = customRangeDay(to, "to", zone, operation);
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 = zone === void 0 ? toB.ms + DAY_MS - 1 : localDayEndMs(toB.y, toB.mo, toB.d, zone);
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 OP2 = "companies.unbilledWork";
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, OP2);
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, OP2);
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, OP2);
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, OP2, billingIncludeFields);
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, OP2, timeFields);
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 OP3 = "contracts.lineOfBusiness";
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(OP3 + ": " + name + " must be a positive integer id (got " + String(value) + ")", { operation: OP3 });
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: OP3, onError: "propagate" });
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 OP11 = PROFITABILITY_OP;
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, OP11, fields));
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, OP11, fields, dateField, v.dateFrom, v.dateTo, wires));
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: OP11, id, idNum, rawHttp, http, wires, windowOn, readPart, windowedPart, truncatedMonths };
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 OP4 = "ticket_charges.moveToTicket";
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 }], OP4, retries);
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", `${OP4}: ticket charge ${sourceId} is billed \u2014 move it in the Autotask UI`, { sourceId, toTicketId }, OP4, {
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: OP4,
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 bool = (v) => typeof v === "boolean" ? v : null;
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: bool(row.isFirstResponseMet), elapsedHours: num2(row.firstResponseElapsedHours) },
11424
- { name: "resolution", met: bool(row.isResolutionMet), elapsedHours: num2(row.resolutionElapsedHours) },
11425
- { name: "resolutionPlan", met: bool(row.isResolutionPlanMet), elapsedHours: num2(row.resolutionPlanElapsedHours) }
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 str = (v) => typeof v === "string" && v !== "" ? v : null;
11594
+ const str2 = (v) => typeof v === "string" && v !== "" ? v : null;
11444
11595
  return {
11445
11596
  ticketId,
11446
- ticketNumber: str(ticket.ticketNumber),
11447
- title: str(ticket.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: str(ticket.createDate),
11458
- dueDateTime: str(ticket.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 OP5 = "tickets.forResource";
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(`${OP5}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP5 });
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: OP5
12375
+ operation: OP6
12225
12376
  });
12226
- const page = unwrapItems(res, OP5);
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(`${OP5}: opts must be an object`, { operation: OP5 });
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(`${OP5}: scope must be 'primary', 'secondary' or 'both' (got ${JSON.stringify(opts.scope)})`, { operation: OP5 });
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(`${OP5}: openOnly must be a boolean`, { operation: OP5 });
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(`${OP5}: limit must be an integer 1-${LIMIT_MAX} (got ${String(opts.limit)})`, { operation: OP5 });
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(`${OP5}: includeFields must be a list of field names`, { operation: OP5 });
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: OP5
12411
+ operation: OP6
12261
12412
  });
12262
- const page = unwrapItems(res, OP5);
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 OP6 = "tickets.lineOfBusiness";
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(OP6 + ": " + name + " must be a positive integer id (got " + String(value) + ")", { operation: OP6 });
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(OP6 + ": the transport is not wired to an AutotaskClient (the composite must run through the client)", { operation: OP6 });
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: OP6, onError: "propagate" });
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 postedDateStatusQuery(_resource, mode, http, opts) {
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 { items } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), operation, http);
12625
- return items;
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 OP11 = opLabel;
12629
- if (opts?.tz !== void 0) validateZone(opts.tz, OP11);
12630
- const fromB = customRangeDay(from, "from", void 0, OP11);
12631
- const toB = customRangeDay(to, "to", void 0, OP11);
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(`${OP11}: from (${iso2(fromB.ms)}) must not be after to (${iso2(toB.ms)})`, { operation: OP11 });
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), OP11, http);
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
- for (const item of items) {
12647
- const postedDate = item.postedDate;
12648
- if (typeof postedDate === "string" ? postedDate.trim() !== "" : postedDate !== null && postedDate !== void 0) {
12649
- postedCount += 1;
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: items.length - postedCount,
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 OP7 = "time_entries.createForResource";
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: OP7 }
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: OP7 }
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: OP7 }
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}`, OP7);
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: OP7,
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: OP7,
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, OP7);
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, OP7);
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: OP7 }
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: OP7,
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}`, OP7);
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, OP7);
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 OP8 = "time_entries.moveToTicket";
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: OP8, maxRetries: retries });
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", `${OP8}: time entry ${sourceId} is on a task \u2014 only ticket time entries are moved`, ids2, OP8);
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", `${OP8}: time entry ${sourceId} has billing approval set \u2014 move it in the Autotask UI`, ids2, OP8, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "approved entries are not moved by the API" } });
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 }], OP8, retries);
13284
+ billing = await queryRows(http, "BillingItems", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP9, retries);
13099
13285
  } catch {
13100
- throw refuse("MOVE_BLOCKED_BILLED", `${OP8}: the billing items of time entry ${sourceId} could not be read \u2014 refused (fails closed)`, ids2, OP8, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billing state unreadable" } });
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", `${OP8}: time entry ${sourceId} is billed or posted \u2014 move it in the Autotask UI`, ids2, OP8, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billed entries are not moved by the API" } });
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 }], OP8, retries);
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", `${OP8}: time entry ${sourceId} has attachments, which a delete would destroy \u2014 move it in the Autotask UI`, ids2, OP8, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "entries with attachments are not moved by the API" } });
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: OP8,
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 OP9 = "time_entries.approve";
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: OP9,
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: OP9,
13393
+ operation: OP10,
13208
13394
  maxRetries: opts.maxRetries
13209
13395
  });
13210
- const levelId = toNumber(parseIdentifier(unwrapOperation(created, OP9).id, "billingItemApprovalLevelID"));
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 OP10 = "time_entries.recentTickets";
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(`${OP10}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP10 });
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(`${OP10}: opts { resourceID, companyID?, tz?, limit? } is required`, { operation: OP10 });
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, OP10);
13251
- const fromB = customRangeDay(from, "from", void 0, OP10);
13252
- const toB = customRangeDay(to, "to", void 0, OP10);
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(`${OP10}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`, { operation: OP10 });
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
- OP10,
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: OP10
13477
+ operation: OP11
13292
13478
  });
13293
- for (const t of unwrapItems(res, OP10).items) if (typeof t.id === "number") tickets.set(t.id, t);
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 postedDateStatusQuery(this, "posted", this.http, opts);
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 postedDateStatusQuery(this, "unposted", this.http, opts);
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) {