@zackbart/connecta 0.35.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +8 -4
- package/bin/version.mjs +1 -1
- package/dist/auth/downstream-oauth.js +71 -79
- package/dist/auth/static-oauth.js +104 -26
- package/dist/connectors/api-connector.d.ts +17 -0
- package/dist/connectors/api-connector.js +3 -1
- package/dist/http/client.js +1 -1
- package/dist/http/hatch-paging.js +2 -2
- package/dist/http/paging.js +3 -89
- package/dist/http/provider.js +1 -2
- package/dist/http/services.js +1 -1
- package/dist/http/tools.js +1 -27
- package/dist/operator-ui/contract.d.ts +1 -0
- package/dist/operator-ui/generated.js +4 -4
- package/dist/program-writes.js +0 -1
- package/dist/providers/_shared/google/kit.js +29 -139
- package/dist/providers/_shared/rest/kit.js +4 -404
- package/dist/providers/_shared/sensitive-fields.js +0 -16
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/ccb/api.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -8
- package/dist/providers/cloudflare/lib/config.d.ts +2 -1
- package/dist/providers/docs/api.js +1 -1
- package/dist/providers/docs/tools.js +1 -2
- package/dist/providers/drive/api.js +2 -2
- package/dist/providers/drive/lib/hatch.js +1 -1
- package/dist/providers/drive/tools.js +1 -1
- package/dist/providers/forms/api.js +1 -1
- package/dist/providers/forms/lib/hatch.js +1 -1
- package/dist/providers/forms/tools.js +2 -3
- package/dist/providers/github/api.js +1 -1
- package/dist/providers/github/lib/discovery.js +2 -2
- package/dist/providers/gmail/api.js +1 -1
- package/dist/providers/gmail/tools.js +2 -2
- package/dist/providers/granola/api.js +155 -0
- package/dist/providers/granola/index.d.ts +10 -0
- package/dist/providers/granola/index.js +68 -0
- package/dist/providers/granola/openapi.generated.js +30 -0
- package/dist/providers/granola/safety.js +37 -0
- package/dist/providers/granola/skill.generated.js +5 -0
- package/dist/providers/granola/tools.js +2 -0
- package/dist/providers/overflow/api.js +26 -26
- package/dist/providers/planning-center/api.js +4 -4
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +2 -2
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +1 -1
- package/dist/providers/sheets/lib/hatch.js +2 -2
- package/dist/providers/sheets/lib/requests.js +1 -1
- package/dist/providers/sheets/tools.js +1 -2
- package/dist/providers/slides/api.js +1 -1
- package/dist/providers/slides/tools.js +2 -2
- package/dist/providers/stripe/api.js +7 -7
- package/dist/providers/tithely/api.js +2 -2
- package/dist/providers/vercel/api.js +12 -12
- package/dist/providers/vercel/index.js +1 -2
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -0
- package/dist/runtime/admission.js +0 -12
- package/dist/runtime/call.js +0 -4
- package/dist/skills.js +0 -1
- package/dist/storage/keys.js +2 -3
- package/dist/types.d.ts +2 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +249 -366
- package/documentation/auth.md +56 -19
- package/documentation/code-mode.md +6 -6
- package/documentation/integrating.md +1 -1
- package/documentation/meta-tools.md +6 -3
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +17 -5
- package/documentation/providers.md +492 -0
- package/package.json +12 -9
- package/templates/node/package.json +1 -1
- package/dist/operator-ui/agents.d.ts +0 -75
- package/dist/operator-ui/agents.js +0 -114
- package/dist/operator-ui/config-summary.js +0 -566
- package/dist/operator-ui/connection-map.js +0 -378
- package/dist/operator-ui/endpoints.js +0 -212
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -79
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -126
- package/dist/operator-ui/setup-commands.js +0 -47
- package/dist/operator-ui/view.d.ts +0 -378
- package/dist/operator-ui/view.js +0 -631
- package/dist/providers/_shared/rest/dispatch.js +0 -21
- package/dist/providers/_shared/rest/tools.js +0 -592
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { keys, optionsOf } from "../../config-schema.js";
|
|
2
|
+
import { PROVIDER_COMMON } from "../../connectors/option-shapes.js";
|
|
3
|
+
import { defineApiProvider } from "../../http/provider.js";
|
|
4
|
+
import { bearer } from "../../http/oauth.js";
|
|
5
|
+
import { apiOrigin, companyInfo, errors, oauth, QuickBooksApi, QUICKBOOKS_INDEX, QUICKBOOKS_NAMED, QUICKBOOKS_VERDICTS, quickbooksBinding, result, } from "./api.js";
|
|
6
|
+
import { quickbooksSafety } from "./safety.js";
|
|
7
|
+
import { quickbooksTools } from "./tools.js";
|
|
8
|
+
import { skill } from "./skill.generated.js";
|
|
9
|
+
const OPTIONS = optionsOf()({
|
|
10
|
+
...PROVIDER_COMMON,
|
|
11
|
+
...keys("realmId", "environment", "clientId", "clientSecret", "defaultPageSize"),
|
|
12
|
+
});
|
|
13
|
+
function baseUrl(options) {
|
|
14
|
+
if (typeof options.realmId !== "string" ||
|
|
15
|
+
options.realmId.trim() !== options.realmId ||
|
|
16
|
+
!/^[1-9]\d{0,30}$/.test(options.realmId))
|
|
17
|
+
throw new Error("quickbooks() realmId must be the fixed numeric company id.");
|
|
18
|
+
if (options.environment !== "sandbox" && options.environment !== "production")
|
|
19
|
+
throw new Error('quickbooks() requires environment: "sandbox" or "production".');
|
|
20
|
+
for (const key of ["clientId", "clientSecret"]) {
|
|
21
|
+
if (typeof options[key] !== "string" || !options[key].trim() || /[\r\n]/.test(options[key]))
|
|
22
|
+
throw new Error(`quickbooks() requires ${key}.`);
|
|
23
|
+
}
|
|
24
|
+
return `${apiOrigin(options)}/v3/company/${options.realmId}`;
|
|
25
|
+
}
|
|
26
|
+
export const quickbooks = defineApiProvider({
|
|
27
|
+
name: "quickbooks",
|
|
28
|
+
title: "QuickBooks Online",
|
|
29
|
+
readme: "QuickBooks Online",
|
|
30
|
+
bundle: {
|
|
31
|
+
baselineGzip: 357549,
|
|
32
|
+
note: "./providers/quickbooks initial read surface measures 357,549 B standalone gzip-9 before recording these bundle facts (Node 26.12.0, neutral workerd/worker/browser/import conditions, esbuild). Shared Effect is paid once per deployment; this provider baseline is reported, not capped (#1079).",
|
|
33
|
+
},
|
|
34
|
+
skill,
|
|
35
|
+
options: OPTIONS,
|
|
36
|
+
displayTitle: (options) => `QuickBooks Online (${options.environment})`,
|
|
37
|
+
description: (options) => `QuickBooks Online accounting, company ${options.realmId} (${options.environment}): ${options.purpose}`,
|
|
38
|
+
vendor: QuickBooksApi,
|
|
39
|
+
baseUrl,
|
|
40
|
+
maxResponseBytes: 4 * 1024 * 1024,
|
|
41
|
+
headers: { Accept: "application/json" },
|
|
42
|
+
pageSize: { default: 25, max: 100 },
|
|
43
|
+
idempotencyKey: false,
|
|
44
|
+
authenticate: bearer,
|
|
45
|
+
oauth,
|
|
46
|
+
errors,
|
|
47
|
+
result,
|
|
48
|
+
safety: quickbooksSafety,
|
|
49
|
+
probe: {
|
|
50
|
+
operation: companyInfo,
|
|
51
|
+
request: (options) => ({
|
|
52
|
+
params: { id: options.realmId },
|
|
53
|
+
query: { minorversion: "75" },
|
|
54
|
+
}),
|
|
55
|
+
describe: () => "Connected to the configured QuickBooks Online company.",
|
|
56
|
+
},
|
|
57
|
+
guide: (options) => ({
|
|
58
|
+
context: [
|
|
59
|
+
`Company ${options.realmId}, ${options.environment}: ${options.purpose}`,
|
|
60
|
+
options.readOnly === false
|
|
61
|
+
? "Bounded named writes are enabled. The API hatch remains read-only."
|
|
62
|
+
: "This connection is read-only. Set readOnly: false in deployment code to enable bounded named writes.",
|
|
63
|
+
"Contact details, bank/card/tax identifiers, credentials and private links are withheld on every read.",
|
|
64
|
+
],
|
|
65
|
+
summary: "QuickBooks Online accounting reads and explicitly enabled customer/service-invoice writes.",
|
|
66
|
+
required: true,
|
|
67
|
+
}),
|
|
68
|
+
hatch: {
|
|
69
|
+
name: "quickbooks",
|
|
70
|
+
title: "QuickBooks Online",
|
|
71
|
+
index: QUICKBOOKS_INDEX,
|
|
72
|
+
verdicts: QUICKBOOKS_VERDICTS,
|
|
73
|
+
named: QUICKBOOKS_NAMED,
|
|
74
|
+
pathExample: "/invoice/42",
|
|
75
|
+
binding: quickbooksBinding,
|
|
76
|
+
notes: () => ({
|
|
77
|
+
read: "Only reviewed record, company and report reads. Use search_records for bounded structured queries; raw SQL is refused.",
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
// The existing common option defaults writable elsewhere. This provider requires explicit opt-in.
|
|
81
|
+
tagConnector: (connector, options) => {
|
|
82
|
+
Object.defineProperty(connector, "readOnly", {
|
|
83
|
+
value: options.readOnly !== false,
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
transformGuide: (guide) => ({
|
|
89
|
+
...guide,
|
|
90
|
+
content: guide.content.replace("Writes take no idempotency key.", "Every named write requires requestId, sent as Intuit's requestid query parameter; no idempotencyKey header argument is accepted."),
|
|
91
|
+
}),
|
|
92
|
+
tools: quickbooksTools,
|
|
93
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ConnectorCallError } from "../../../errors.js";
|
|
3
|
+
import { isMaxChars } from "../../../http/json-schema.js";
|
|
4
|
+
export const matches = (pattern) => Schema.makeFilter((value) => new RegExp(pattern).test(value), { toJsonSchema: () => ({ pattern }) });
|
|
5
|
+
const DateOnly = Schema.String.check(matches("^\\d{4}-\\d{2}-\\d{2}$"));
|
|
6
|
+
export const REPORT_QUERY = {
|
|
7
|
+
start_date: Schema.optionalKey(DateOnly.annotate({ description: "First date, YYYY-MM-DD; P&L and Cash Flow only." })),
|
|
8
|
+
end_date: Schema.optionalKey(DateOnly.annotate({ description: "Last date or statement date, YYYY-MM-DD." })),
|
|
9
|
+
accounting_method: Schema.optionalKey(Schema.Literals(["Cash", "Accrual"]).annotate({ description: "Accounting basis; P&L and Balance Sheet only." })),
|
|
10
|
+
};
|
|
11
|
+
export function reportQuery(report) {
|
|
12
|
+
if (report === "AgedReceivables" || report === "AgedPayables")
|
|
13
|
+
return {
|
|
14
|
+
report_date: Schema.optionalKey(DateOnly.annotate({ description: "As-of date, YYYY-MM-DD." })),
|
|
15
|
+
aging_method: Schema.optionalKey(Schema.Literal("Report_Date").annotate({ description: "Age relative to report_date." })),
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
...(["ProfitAndLoss", "CashFlow"].includes(report) ? { start_date: REPORT_QUERY.start_date } : {}),
|
|
19
|
+
end_date: REPORT_QUERY.end_date,
|
|
20
|
+
...(["ProfitAndLoss", "BalanceSheet"].includes(report)
|
|
21
|
+
? { accounting_method: REPORT_QUERY.accounting_method }
|
|
22
|
+
: {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Named reports use one end_date; aging translates it to the vendor's snapshot date. */
|
|
26
|
+
export function reportParameters(report, query) {
|
|
27
|
+
if ((report === "AgedReceivables" || report === "AgedPayables") && query["end_date"] !== undefined) {
|
|
28
|
+
const { end_date, ...rest } = query;
|
|
29
|
+
return { ...rest, report_date: end_date, aging_method: "Report_Date" };
|
|
30
|
+
}
|
|
31
|
+
return query;
|
|
32
|
+
}
|
|
33
|
+
export function reportProblem(report, query) {
|
|
34
|
+
const allowed = new Set(["minorversion", ...Object.keys(reportQuery(report))]);
|
|
35
|
+
if (Object.keys(query).some((key) => !allowed.has(key)))
|
|
36
|
+
return "This report does not support a supplied parameter; inspect get_report or quickbooks_api_details.";
|
|
37
|
+
for (const key of ["start_date", "end_date", "report_date"]) {
|
|
38
|
+
const value = query[key];
|
|
39
|
+
if (value !== undefined &&
|
|
40
|
+
(typeof value !== "string" ||
|
|
41
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(value) ||
|
|
42
|
+
!Number.isFinite(Date.parse(value)) ||
|
|
43
|
+
new Date(value).toISOString().slice(0, 10) !== value))
|
|
44
|
+
return `${key} must be a real YYYY-MM-DD date.`;
|
|
45
|
+
}
|
|
46
|
+
if (query["start_date"] && query["end_date"] && String(query["start_date"]) > String(query["end_date"]))
|
|
47
|
+
return "start_date must not follow end_date.";
|
|
48
|
+
if (query["aging_method"] !== undefined && query["report_date"] === undefined)
|
|
49
|
+
return "aging_method requires report_date.";
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const FILTER_FIELDS = [
|
|
53
|
+
"Id",
|
|
54
|
+
"DisplayName",
|
|
55
|
+
"Name",
|
|
56
|
+
"Active",
|
|
57
|
+
"MetaData.LastUpdatedTime",
|
|
58
|
+
"TxnDate",
|
|
59
|
+
"DueDate",
|
|
60
|
+
"DocNumber",
|
|
61
|
+
"CustomerRef",
|
|
62
|
+
"VendorRef",
|
|
63
|
+
"Balance",
|
|
64
|
+
];
|
|
65
|
+
export const Filters = Schema.Array(Schema.Struct({
|
|
66
|
+
field: Schema.Literals(FILTER_FIELDS).annotate({
|
|
67
|
+
description: "Reviewed filter field, subject to the chosen record type.",
|
|
68
|
+
}),
|
|
69
|
+
operator: Schema.Literals(["eq", "gt", "gte", "lt", "lte"]).annotate({
|
|
70
|
+
description: "Equality or an ordered date/amount comparison.",
|
|
71
|
+
}),
|
|
72
|
+
value: Schema.Union([Schema.String.check(isMaxChars(200)), Schema.Boolean, Schema.Number.check(Schema.isFinite())]),
|
|
73
|
+
})).check(Schema.isMaxLength(8));
|
|
74
|
+
const operations = { eq: "=", gt: ">", gte: ">=", lt: "<", lte: "<=" };
|
|
75
|
+
/** One SQL builder. No agent text is ever used as a clause, identifier or paging anchor. */
|
|
76
|
+
export function buildSearch(entity, filters, start, size) {
|
|
77
|
+
const master = ["Customer", "Vendor", "Account", "Item"].includes(entity);
|
|
78
|
+
const fields = new Set([
|
|
79
|
+
"Id",
|
|
80
|
+
"MetaData.LastUpdatedTime",
|
|
81
|
+
...(master
|
|
82
|
+
? ["Active", entity === "Customer" || entity === "Vendor" ? "DisplayName" : "Name"]
|
|
83
|
+
: [
|
|
84
|
+
"TxnDate",
|
|
85
|
+
...(entity === "Payment" ? [] : ["DocNumber"]),
|
|
86
|
+
...(entity === "Invoice" || entity === "Bill" ? ["DueDate", "Balance"] : []),
|
|
87
|
+
...(entity === "Invoice" || entity === "Payment" ? ["CustomerRef"] : ["VendorRef"]),
|
|
88
|
+
]),
|
|
89
|
+
]);
|
|
90
|
+
if (!Number.isSafeInteger(start) || start < 1 || !Number.isSafeInteger(size) || size < 1 || size > 101)
|
|
91
|
+
throw new ConnectorCallError("invalid_args", "Invalid QuickBooks search page.");
|
|
92
|
+
const clauses = filters.map(({ field, operator, value }) => {
|
|
93
|
+
if (!fields.has(field))
|
|
94
|
+
throw new ConnectorCallError("invalid_args", `${field} is not a reviewed filter for ${entity}.`);
|
|
95
|
+
const range = ["TxnDate", "DueDate", "MetaData.LastUpdatedTime", "Balance"].includes(field);
|
|
96
|
+
if (!range && operator !== "eq")
|
|
97
|
+
throw new ConnectorCallError("invalid_args", `${field} supports only eq.`);
|
|
98
|
+
if (field === "Active"
|
|
99
|
+
? typeof value !== "boolean"
|
|
100
|
+
: field === "Balance"
|
|
101
|
+
? typeof value !== "number"
|
|
102
|
+
: typeof value !== "string" ||
|
|
103
|
+
[...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
|
|
104
|
+
throw new ConnectorCallError("invalid_args", `Invalid value type for ${field}.`);
|
|
105
|
+
if (["Id", "CustomerRef", "VendorRef"].includes(field) && !/^[1-9]\d{0,30}$/.test(String(value)))
|
|
106
|
+
throw new ConnectorCallError("invalid_args", `${field} requires a numeric record id.`);
|
|
107
|
+
if (["TxnDate", "DueDate"].includes(field) && reportProblem("ProfitAndLoss", { end_date: value }))
|
|
108
|
+
throw new ConnectorCallError("invalid_args", `${field} requires a real YYYY-MM-DD date.`);
|
|
109
|
+
if (field === "MetaData.LastUpdatedTime" &&
|
|
110
|
+
(typeof value !== "string" ||
|
|
111
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) ||
|
|
112
|
+
!Number.isFinite(Date.parse(value))))
|
|
113
|
+
throw new ConnectorCallError("invalid_args", "LastUpdatedTime requires an ISO timestamp with timezone.");
|
|
114
|
+
const literal = typeof value === "string" ? `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'` : String(value);
|
|
115
|
+
return `${field} ${operations[operator]} ${literal}`;
|
|
116
|
+
});
|
|
117
|
+
return `SELECT * FROM ${entity}${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} STARTPOSITION ${start} MAXRESULTS ${size}`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ConnectorCallError } from "../../../errors.js";
|
|
3
|
+
import { Refusal, StatusFailure } from "../../../http/errors.js";
|
|
4
|
+
import { Budget } from "../../../http/services.js";
|
|
5
|
+
import { write } from "../../../http/tools.js";
|
|
6
|
+
import { companyInfo, CustomerNames, customerWrite, invoiceWrite, PublicId, recordReads, SyncToken } from "../api.js";
|
|
7
|
+
import { namedRecord, NamedRecordShape, object } from "../safety.js";
|
|
8
|
+
import { matches, reportProblem } from "./query.js";
|
|
9
|
+
const RequestId = Schema.String.check(matches("^[A-Za-z0-9][A-Za-z0-9_-]{0,49}$")).annotate({
|
|
10
|
+
description: "Stable identifier for this exact intent, unique across all requests in this company; at most 50 ASCII letters, digits, underscores or hyphens. Never reuse for changed content.",
|
|
11
|
+
});
|
|
12
|
+
const DateOnly = Schema.String.check(matches("^\\d{4}-\\d{2}-\\d{2}$"));
|
|
13
|
+
// Decimal strings avoid binary arithmetic and caller-side cents conversion. Conversion only encodes a JSON number.
|
|
14
|
+
const Amount = Schema.String.check(matches("^(?:0|[1-9]\\d{0,8})\\.\\d{2}$")).annotate({
|
|
15
|
+
description: "Positive decimal currency units with exactly two fractional digits, at most 999999999.99; never integer cents. One service unit per line.",
|
|
16
|
+
});
|
|
17
|
+
const InvoiceInput = Schema.Struct({
|
|
18
|
+
requestId: RequestId,
|
|
19
|
+
customerId: PublicId.annotate({ description: "Caller-observed active Customer Id in this company." }),
|
|
20
|
+
currency: Schema.Literal("USD").annotate({
|
|
21
|
+
description: "Caller-observed customer currency, restricted to USD for this initial two-decimal write surface. Vendor computes taxes and totals.",
|
|
22
|
+
}),
|
|
23
|
+
exchangeRate: Schema.String.check(matches("^(?:0|[1-9]\\d{0,5})(?:\\.\\d{1,6})?$")).annotate({
|
|
24
|
+
description: "Explicit positive home-currency units per transaction-currency unit, at most six decimal places. Use 1 for home currency; no rate is fetched or defaulted.",
|
|
25
|
+
}),
|
|
26
|
+
txnDate: DateOnly.annotate({ description: "Explicit accounting date, YYYY-MM-DD." }),
|
|
27
|
+
dueDate: DateOnly.annotate({ description: "Explicit due date, YYYY-MM-DD, on or after txnDate." }),
|
|
28
|
+
docNumber: Schema.String.check(matches("^[A-Za-z0-9][A-Za-z0-9_-]{0,20}$")).annotate({
|
|
29
|
+
description: "Explicit reconciliation label, at most 21 characters; not AUTO_GENERATE. Labels may repeat, so reconcile customer, currency and amounts too.",
|
|
30
|
+
}),
|
|
31
|
+
lines: Schema.Array(Schema.Struct({
|
|
32
|
+
itemId: PublicId.annotate({
|
|
33
|
+
description: "Caller-observed active Service item Id; inventory and other item types are refused.",
|
|
34
|
+
}),
|
|
35
|
+
amount: Amount,
|
|
36
|
+
})).check(Schema.isMinLength(1), Schema.isMaxLength(20)),
|
|
37
|
+
});
|
|
38
|
+
const refuse = (message) => Effect.fail(new Refusal({ error: new ConnectorCallError("invalid_args", message) }));
|
|
39
|
+
const version = { minorversion: "75" };
|
|
40
|
+
const charge = Effect.flatMap(Budget, (budget) => budget.charge).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)));
|
|
41
|
+
/** Totals and tax are vendor-computed; the authored references, dates and service prices must agree. */
|
|
42
|
+
function matchesInvoice(record, args) {
|
|
43
|
+
const lines = Array.isArray(record["Line"]) ? record["Line"] : [];
|
|
44
|
+
const services = lines.filter((line) => object(line)["DetailType"] === "SalesItemLineDetail");
|
|
45
|
+
const expected = args.lines
|
|
46
|
+
.map((line) => JSON.stringify([line.itemId, Number(line.amount), 1, Number(line.amount)]))
|
|
47
|
+
.sort();
|
|
48
|
+
const actual = services
|
|
49
|
+
.map((line) => {
|
|
50
|
+
const row = object(line), detail = object(row["SalesItemLineDetail"]);
|
|
51
|
+
return JSON.stringify([object(detail["ItemRef"])["value"], row["Amount"], detail["Qty"], detail["UnitPrice"]]);
|
|
52
|
+
})
|
|
53
|
+
.sort();
|
|
54
|
+
return (object(record["CustomerRef"])["value"] === args.customerId &&
|
|
55
|
+
object(record["CurrencyRef"])["value"] === args.currency &&
|
|
56
|
+
record["DocNumber"] === args.docNumber &&
|
|
57
|
+
record["TxnDate"] === args.txnDate &&
|
|
58
|
+
record["DueDate"] === args.dueDate &&
|
|
59
|
+
(record["ExchangeRate"] === Number(args.exchangeRate) ||
|
|
60
|
+
(record["ExchangeRate"] === undefined && Number(args.exchangeRate) === 1)) &&
|
|
61
|
+
lines.every((line) => ["SalesItemLineDetail", "SubTotalLineDetail"].includes(String(object(line)["DetailType"]))) &&
|
|
62
|
+
JSON.stringify(actual) === JSON.stringify(expected));
|
|
63
|
+
}
|
|
64
|
+
export function quickbooksWrites(connection, realmId) {
|
|
65
|
+
return [
|
|
66
|
+
write(connection, {
|
|
67
|
+
name: "create_customer",
|
|
68
|
+
description: "Create a customer once with a DisplayName and optional CompanyName. No contact, tax, bank or payment fields. Reconcile an unknown outcome by exact DisplayName before another intent.",
|
|
69
|
+
destructive: true,
|
|
70
|
+
idempotent: false,
|
|
71
|
+
input: Schema.Struct({ requestId: RequestId, ...CustomerNames }),
|
|
72
|
+
output: NamedRecordShape,
|
|
73
|
+
run: ({ requestId, ...body }) => connection
|
|
74
|
+
.send(customerWrite, { query: { ...version, requestid: requestId }, payload: body })
|
|
75
|
+
.pipe(Effect.map(({ data }) => namedRecord(object(data)["Customer"]))),
|
|
76
|
+
}),
|
|
77
|
+
write(connection, {
|
|
78
|
+
name: "update_customer",
|
|
79
|
+
description: "Sparsely update only DisplayName and optional CompanyName using caller-observed Id and SyncToken. A stale version fails; no version fetch or overwrite follows. Omitted CompanyName is unchanged.",
|
|
80
|
+
destructive: true,
|
|
81
|
+
idempotent: false,
|
|
82
|
+
input: Schema.Struct({ requestId: RequestId, Id: PublicId, SyncToken, ...CustomerNames }),
|
|
83
|
+
output: NamedRecordShape,
|
|
84
|
+
run: ({ requestId, ...body }) => connection
|
|
85
|
+
.send(customerWrite, { query: { ...version, requestid: requestId }, payload: { ...body, sparse: true } })
|
|
86
|
+
.pipe(Effect.map(({ data }) => namedRecord(object(data)["Customer"]))),
|
|
87
|
+
}),
|
|
88
|
+
write(connection, {
|
|
89
|
+
name: "create_invoice",
|
|
90
|
+
destructive: true,
|
|
91
|
+
idempotent: false,
|
|
92
|
+
description: "Create a real US-company USD accounting invoice, never a draft, once after checking company, active customer/currency and Service items. Dates, DocNumber, exchange rate and one-unit line prices are explicit. EmailStatus is fixed NotSet and online payment flags are false; no email/send operation. Vendor computes taxes/totals. Reconcile unknown outcomes through reads; never blindly create again.",
|
|
93
|
+
input: InvoiceInput,
|
|
94
|
+
output: NamedRecordShape,
|
|
95
|
+
writeSteps: {
|
|
96
|
+
company: "once",
|
|
97
|
+
customer: "once",
|
|
98
|
+
items: {
|
|
99
|
+
maxItems: 20,
|
|
100
|
+
items: (args) => [...new Set(args.lines.map((line) => line.itemId))],
|
|
101
|
+
request: (item) => ({ method: "GET", path: `/item/${item}`, template: "/item/:id", query: version }),
|
|
102
|
+
},
|
|
103
|
+
write: "once",
|
|
104
|
+
},
|
|
105
|
+
run: (args) => Effect.gen(function* () {
|
|
106
|
+
const problem = reportProblem("ProfitAndLoss", { start_date: args.txnDate, end_date: args.dueDate });
|
|
107
|
+
if (problem)
|
|
108
|
+
return yield* refuse("Invoice dates must be real YYYY-MM-DD dates, with dueDate on or after txnDate.");
|
|
109
|
+
if (args.docNumber === "AUTO_GENERATE" ||
|
|
110
|
+
Number(args.exchangeRate) <= 0 ||
|
|
111
|
+
args.lines.some((line) => Number(line.amount) <= 0))
|
|
112
|
+
return yield* refuse("Use an explicit DocNumber and positive exchange rate and line amounts.");
|
|
113
|
+
const company = object(object((yield* connection.send(companyInfo, { params: { id: realmId }, query: version }, { step: "company", effect: "read" })).data)["CompanyInfo"]);
|
|
114
|
+
if (company["Id"] !== realmId || company["Country"] !== "US")
|
|
115
|
+
return yield* refuse("Invoice creation is restricted to US companies, where the reviewed invoice-autosend contract applies.");
|
|
116
|
+
yield* charge;
|
|
117
|
+
const customer = object(object((yield* connection.send(recordReads["customers"], { params: { id: args.customerId }, query: version }, { step: "customer", effect: "read" })).data)["Customer"]);
|
|
118
|
+
if (customer["Id"] !== args.customerId ||
|
|
119
|
+
customer["Active"] !== true ||
|
|
120
|
+
object(customer["CurrencyRef"])["value"] !== args.currency)
|
|
121
|
+
return yield* refuse("The customer must be active and have the supplied currency; reread before creating an invoice.");
|
|
122
|
+
for (const itemId of new Set(args.lines.map((line) => line.itemId))) {
|
|
123
|
+
yield* charge;
|
|
124
|
+
const item = object(object((yield* connection.send(recordReads["items"], { params: { id: itemId }, query: version }, { step: "items", item: itemId, effect: "read" })).data)["Item"]);
|
|
125
|
+
if (item["Id"] !== itemId || item["Active"] !== true || item["Type"] !== "Service")
|
|
126
|
+
return yield* refuse("Every invoice item must be an active Service item; reread before creating an invoice.");
|
|
127
|
+
}
|
|
128
|
+
yield* charge;
|
|
129
|
+
const { data, status } = yield* connection.send(invoiceWrite, {
|
|
130
|
+
query: { ...version, requestid: args.requestId },
|
|
131
|
+
payload: {
|
|
132
|
+
CustomerRef: { value: args.customerId },
|
|
133
|
+
CurrencyRef: { value: args.currency },
|
|
134
|
+
ExchangeRate: Number(args.exchangeRate),
|
|
135
|
+
TxnDate: args.txnDate,
|
|
136
|
+
DueDate: args.dueDate,
|
|
137
|
+
DocNumber: args.docNumber,
|
|
138
|
+
Line: args.lines.map((line) => ({
|
|
139
|
+
Amount: Number(line.amount),
|
|
140
|
+
DetailType: "SalesItemLineDetail",
|
|
141
|
+
SalesItemLineDetail: {
|
|
142
|
+
ItemRef: { value: line.itemId },
|
|
143
|
+
Qty: 1,
|
|
144
|
+
UnitPrice: Number(line.amount),
|
|
145
|
+
},
|
|
146
|
+
})),
|
|
147
|
+
EmailStatus: "NotSet",
|
|
148
|
+
AllowIPNPayment: false,
|
|
149
|
+
AllowOnlinePayment: false,
|
|
150
|
+
AllowOnlineCreditCardPayment: false,
|
|
151
|
+
AllowOnlineACHPayment: false,
|
|
152
|
+
AllowOnlinePayPalPayment: false,
|
|
153
|
+
AllowOnlineAffirmPayment: false,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const invoice = object(object(data)["Invoice"]);
|
|
157
|
+
if (!matchesInvoice(invoice, args))
|
|
158
|
+
return yield* Effect.fail(new StatusFailure({
|
|
159
|
+
status,
|
|
160
|
+
headers: new Headers(),
|
|
161
|
+
body: undefined,
|
|
162
|
+
operation: { method: "POST", template: "/invoice" },
|
|
163
|
+
}));
|
|
164
|
+
return namedRecord(invoice);
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
3
|
+
import { withholdEmbeddedUrls } from "../_shared/rest/value-safety.js";
|
|
4
|
+
import { QUICKBOOKS_INDEX } from "./api.js";
|
|
5
|
+
const scalars = [
|
|
6
|
+
"Id",
|
|
7
|
+
"SyncToken",
|
|
8
|
+
"DisplayName",
|
|
9
|
+
"Name",
|
|
10
|
+
"CompanyName",
|
|
11
|
+
"Active",
|
|
12
|
+
"FullyQualifiedName",
|
|
13
|
+
"AccountType",
|
|
14
|
+
"AccountSubType",
|
|
15
|
+
"Classification",
|
|
16
|
+
"CurrentBalance",
|
|
17
|
+
"CurrentBalanceWithSubAccounts",
|
|
18
|
+
"Type",
|
|
19
|
+
"UnitPrice",
|
|
20
|
+
"PurchaseCost",
|
|
21
|
+
"QtyOnHand",
|
|
22
|
+
"TrackQtyOnHand",
|
|
23
|
+
"TxnDate",
|
|
24
|
+
"DueDate",
|
|
25
|
+
"DocNumber",
|
|
26
|
+
"TotalAmt",
|
|
27
|
+
"Balance",
|
|
28
|
+
"UnappliedAmt",
|
|
29
|
+
"ExchangeRate",
|
|
30
|
+
"HomeTotalAmt",
|
|
31
|
+
"HomeBalance",
|
|
32
|
+
"Amount",
|
|
33
|
+
"LineNum",
|
|
34
|
+
"DetailType",
|
|
35
|
+
"Qty",
|
|
36
|
+
"UnitBasedOn",
|
|
37
|
+
"PercentBased",
|
|
38
|
+
"DiscountPercent",
|
|
39
|
+
"DiscountAmt",
|
|
40
|
+
"TotalTax",
|
|
41
|
+
"TaxInclusiveAmt",
|
|
42
|
+
"PayType",
|
|
43
|
+
];
|
|
44
|
+
const references = [
|
|
45
|
+
"CurrencyRef",
|
|
46
|
+
"CustomerRef",
|
|
47
|
+
"VendorRef",
|
|
48
|
+
"ParentRef",
|
|
49
|
+
"AccountRef",
|
|
50
|
+
"IncomeAccountRef",
|
|
51
|
+
"ExpenseAccountRef",
|
|
52
|
+
"AssetAccountRef",
|
|
53
|
+
"APAccountRef",
|
|
54
|
+
"ARAccountRef",
|
|
55
|
+
"DepositToAccountRef",
|
|
56
|
+
"ItemRef",
|
|
57
|
+
"ClassRef",
|
|
58
|
+
"DepartmentRef",
|
|
59
|
+
"SalesTermRef",
|
|
60
|
+
];
|
|
61
|
+
const details = [
|
|
62
|
+
"SalesItemLineDetail",
|
|
63
|
+
"AccountBasedExpenseLineDetail",
|
|
64
|
+
"ItemBasedExpenseLineDetail",
|
|
65
|
+
"DiscountLineDetail",
|
|
66
|
+
"SubTotalLineDetail",
|
|
67
|
+
"GroupLineDetail",
|
|
68
|
+
"TxnTaxDetail",
|
|
69
|
+
];
|
|
70
|
+
const optional = (names) => Object.fromEntries(names.map((key) => [key, Schema.optionalKey(Schema.Unknown)]));
|
|
71
|
+
export const RecordShape = Schema.Struct({
|
|
72
|
+
...optional(scalars),
|
|
73
|
+
...optional(references),
|
|
74
|
+
...optional(details),
|
|
75
|
+
MetaData: Schema.optionalKey(Schema.Unknown),
|
|
76
|
+
Line: Schema.optionalKey(Schema.Unknown),
|
|
77
|
+
LinkedTxn: Schema.optionalKey(Schema.Unknown),
|
|
78
|
+
});
|
|
79
|
+
export const CompanyShape = Schema.Struct(optional(["Id", "SyncToken", "CompanyName", "LegalName", "Country", "FiscalYearStartMonth", "SupportedLanguages"]));
|
|
80
|
+
const namedKeys = [
|
|
81
|
+
"Id",
|
|
82
|
+
"SyncToken",
|
|
83
|
+
"DisplayName",
|
|
84
|
+
"Name",
|
|
85
|
+
"CompanyName",
|
|
86
|
+
"Active",
|
|
87
|
+
"AccountType",
|
|
88
|
+
"CurrentBalance",
|
|
89
|
+
"UnitPrice",
|
|
90
|
+
"QtyOnHand",
|
|
91
|
+
"PayType",
|
|
92
|
+
"Type",
|
|
93
|
+
"TxnDate",
|
|
94
|
+
"DueDate",
|
|
95
|
+
"DocNumber",
|
|
96
|
+
"TotalAmt",
|
|
97
|
+
"Balance",
|
|
98
|
+
"UnappliedAmt",
|
|
99
|
+
"CurrencyRef",
|
|
100
|
+
"ExchangeRate",
|
|
101
|
+
"CustomerRef",
|
|
102
|
+
"VendorRef",
|
|
103
|
+
"MetaData",
|
|
104
|
+
"Line",
|
|
105
|
+
"LinkedTxn",
|
|
106
|
+
];
|
|
107
|
+
export const NamedRecordShape = Schema.Struct(optional(namedKeys));
|
|
108
|
+
export const namedRecord = (value) => Object.fromEntries(Object.entries(object(value)).filter(([key]) => namedKeys.includes(key)));
|
|
109
|
+
export const ReportShape = Schema.Struct({
|
|
110
|
+
Header: Schema.optionalKey(Schema.Unknown),
|
|
111
|
+
Columns: Schema.optionalKey(Schema.Unknown),
|
|
112
|
+
Rows: Schema.optionalKey(Schema.Unknown),
|
|
113
|
+
});
|
|
114
|
+
export const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
115
|
+
const cleanText = (text) => withholdEmbeddedUrls(text).replace(/https?:\/\/[^\s<>"']+/gi, "[redacted]");
|
|
116
|
+
const scalar = (value) => typeof value === "string"
|
|
117
|
+
? cleanText(value)
|
|
118
|
+
: (typeof value === "number" && Number.isFinite(value)) || typeof value === "boolean"
|
|
119
|
+
? value
|
|
120
|
+
: undefined;
|
|
121
|
+
function pick(value, keys) {
|
|
122
|
+
const source = object(value);
|
|
123
|
+
return Object.fromEntries(keys.flatMap((key) => source[key] === undefined || scalar(source[key]) === undefined ? [] : [[key, scalar(source[key])]]));
|
|
124
|
+
}
|
|
125
|
+
/** Closed accounting projection: unknown fields never become a way around privacy. SyncToken is a version, not a credential. */
|
|
126
|
+
function safeRecord(value) {
|
|
127
|
+
const source = object(value), out = pick(source, scalars);
|
|
128
|
+
for (const key of references)
|
|
129
|
+
if (source[key])
|
|
130
|
+
out[key] = pick(source[key], ["value", "name", "type"]);
|
|
131
|
+
if (source["MetaData"])
|
|
132
|
+
out["MetaData"] = pick(source["MetaData"], ["CreateTime", "LastUpdatedTime"]);
|
|
133
|
+
for (const key of details)
|
|
134
|
+
if (source[key])
|
|
135
|
+
out[key] = safeRecord(source[key]);
|
|
136
|
+
if (Array.isArray(source["Line"]))
|
|
137
|
+
out["Line"] = source["Line"].map(safeRecord);
|
|
138
|
+
if (Array.isArray(source["LinkedTxn"]))
|
|
139
|
+
out["LinkedTxn"] = source["LinkedTxn"].map((row) => pick(row, ["TxnId", "TxnType", "TxnLineId"]));
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
// Report trees retain vendor hierarchy, column metadata and empty strings. No index has a fixed metric meaning.
|
|
143
|
+
function reportTree(value) {
|
|
144
|
+
if (Array.isArray(value))
|
|
145
|
+
return value.map(reportTree);
|
|
146
|
+
if (typeof value !== "object" || value === null)
|
|
147
|
+
return scalar(value);
|
|
148
|
+
const keys = new Set([
|
|
149
|
+
"Header",
|
|
150
|
+
"Columns",
|
|
151
|
+
"Column",
|
|
152
|
+
"Rows",
|
|
153
|
+
"Row",
|
|
154
|
+
"Summary",
|
|
155
|
+
"ColData",
|
|
156
|
+
"ColTitle",
|
|
157
|
+
"ColType",
|
|
158
|
+
"MetaData",
|
|
159
|
+
"Name",
|
|
160
|
+
"Value",
|
|
161
|
+
"Option",
|
|
162
|
+
"Time",
|
|
163
|
+
"ReportName",
|
|
164
|
+
"DateMacro",
|
|
165
|
+
"ReportBasis",
|
|
166
|
+
"StartPeriod",
|
|
167
|
+
"EndPeriod",
|
|
168
|
+
"SummarizeColumnsBy",
|
|
169
|
+
"Currency",
|
|
170
|
+
"value",
|
|
171
|
+
"id",
|
|
172
|
+
"type",
|
|
173
|
+
"group",
|
|
174
|
+
]);
|
|
175
|
+
return Object.fromEntries(Object.entries(value)
|
|
176
|
+
.filter(([key]) => keys.has(key))
|
|
177
|
+
.map(([key, entry]) => [key, reportTree(entry)]));
|
|
178
|
+
}
|
|
179
|
+
export function quickbooksSafety(options) {
|
|
180
|
+
return {
|
|
181
|
+
reviewed: {
|
|
182
|
+
resolve: (method, path) => {
|
|
183
|
+
if (/[%;\\?#]/.test(path) || path.split("/").some((part) => part === "." || part === ".."))
|
|
184
|
+
throw new ConnectorCallError("invalid_args", "Use a canonical reviewed QuickBooks path.");
|
|
185
|
+
const { op } = QUICKBOOKS_INDEX.resolve(method, path);
|
|
186
|
+
if (op.path === "/companyinfo/{id}" && path !== `/companyinfo/${options.realmId}`)
|
|
187
|
+
throw new ConnectorCallError("invalid_args", "The company must match the configured realmId.");
|
|
188
|
+
return { method: op.method, template: op.path };
|
|
189
|
+
},
|
|
190
|
+
withholdsErrors: () => true,
|
|
191
|
+
redact: (body, operation) => {
|
|
192
|
+
const root = object(body);
|
|
193
|
+
if (operation.template.startsWith("/reports/"))
|
|
194
|
+
return reportTree(root);
|
|
195
|
+
if (operation.template.startsWith("/companyinfo/"))
|
|
196
|
+
return {
|
|
197
|
+
CompanyInfo: pick(root["CompanyInfo"], [
|
|
198
|
+
"Id",
|
|
199
|
+
"SyncToken",
|
|
200
|
+
"CompanyName",
|
|
201
|
+
"LegalName",
|
|
202
|
+
"Country",
|
|
203
|
+
"FiscalYearStartMonth",
|
|
204
|
+
"SupportedLanguages",
|
|
205
|
+
]),
|
|
206
|
+
};
|
|
207
|
+
if (operation.template === "/query") {
|
|
208
|
+
const query = object(root["QueryResponse"]);
|
|
209
|
+
return {
|
|
210
|
+
QueryResponse: Object.fromEntries(Object.entries(query)
|
|
211
|
+
.filter(([key]) => ["Customer", "Vendor", "Account", "Item", "Invoice", "Bill", "Payment", "BillPayment"].includes(key) && Array.isArray(query[key]))
|
|
212
|
+
.map(([key, rows]) => [key, rows.map(safeRecord)])),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return Object.fromEntries(Object.entries(root)
|
|
216
|
+
.filter(([key]) => ["Customer", "Vendor", "Account", "Item", "Invoice", "Bill", "Payment", "BillPayment"].includes(key))
|
|
217
|
+
.map(([key, record]) => [key, safeRecord(record)]));
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
sensitiveFields: { names: [], choice: "drop" },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
|
+
export const skill = {
|
|
3
|
+
"content": "## Accounting identity and joins\n\nAn Id identifies a record inside this company and record family. DocNumber is\na human label, may repeat, and is not an Id. SyncToken is the public record\nversion used for concurrency control, not an OAuth credential. References carry\nthe target Id in value; invoice CustomerRef joins Customer, bill VendorRef\njoins Vendor, and payment Line.LinkedTxn joins invoices or bills.\n\nAccounting Payment and BillPayment records describe received and paid money;\nreading them does not execute a payment. Desktop, Payments, Payroll, attachments,\nand unreviewed accounting entities are outside this connection.\n\n## Money, time and reports\n\nAmounts use decimal currency units, never integer cents. Preserve CurrencyRef\nand ExchangeRate when combining transactions; never sum unlike currencies.\nTxnDate is the company's accounting date, not a UTC instant. MetaData timestamps\ncarry their own offset. Customer and vendor searches omit inactive records\nunless the Active filter explicitly selects them.\n\nReports are trees with Header, Columns, nested Rows and section summaries.\nRead column types and metadata together with row values; column order and\nsection labels can change. Empty amount strings mean no value, not zero.\nDo not flatten away section identity or add totals to their component rows.\nCheck the returned period, basis, currency and NoReportData option before\ncomparing reports. Aging is an as-of snapshot, rather than a transaction range.\n\nReturn aggregates and the few identifying records the task needs; avoid\ncopying full customer or vendor directories into the conversation.\n\n\n## Named writes and reconciliation\n\nThe connector defaults to read-only. Deployment code must set `readOnly: false`\nto expose named writes; endpoint and pool read-only policies still apply. The\nAPI hatch remains read-only. `create_customer` accepts DisplayName and optional\nCompanyName. `update_customer` requires caller-observed Id and SyncToken and\nchanges only those naming fields with sparse:true. Omitted CompanyName stays\nunchanged. A stale-version conflict never fetches a newer version or overwrites\nit. Read and review the current record before proposing a new update.\nDisplayName must be unique across customers, vendors and employees.\n\n`create_invoice` creates a real accounting transaction, never a draft, for a\nverified US company and a customer using USD. This bounds the no-email claim\nto the reviewed US autosend contract and money to two-decimal USD units. First\nobserve an active customer with its CurrencyRef and active Service item Ids.\nSupply currency USD, a positive explicit exchange rate, accounting and\ndue dates, a reconciliation DocNumber and 1–20 lines. Each line is one service\nunit with an explicit positive amount in decimal currency units, supplied as a\nstring with exactly two fractional digits. No prices, dates or exchange rates\nare defaulted or fetched. Preflight reads verify the fixed company Country is US\nand recheck customer currency and item activity/type; they are not an atomic snapshot. Vendor validation can still\nreject changed references. Taxes and totals are computed by QuickBooks: inspect\nthe returned currency, exchange rate and totals; line sums are not a promise of\nan untaxed total. Locale/company tax requirements may reject this narrow body.\nNo customer/item creation happens implicitly.\nEach additional request charges the call-admission budget before dispatch.\nContradictory returned references, dates, rate or service prices produce an\nunknown outcome; reconcile through reads rather than repeating the write.\n\nInvoice email status is fixed to NotSet; online, IPN, credit-card, ACH, PayPal\nand Affirm payment flags are fixed false. No email field or send operation is\nexposed. Company invoice-autosend opt-in does not supply the documented\nNeedToSend request state. This prevents this creation request from asking Intuit\nto email the invoice, including when the customer has an email on file.\nInvoice updates are unavailable: preserving an existing send-ready state has\nnot been established safe, while resetting it would change payment settings.\nDeletes, voids, email delivery, charges, refunds, other accounting writes and\narbitrary mutations are unavailable.\n\nEvery write requires a caller-generated `requestId`: 1–50 ASCII letters,\ndigits, underscores or hyphens, starting with a letter or digit. Keep it stable\nfor the exact intent and unique across requests/tools for this company. It is\nsent in Intuit's requestid query parameter with minorversion 75, rather than an\nidempotency header. Intuit documents recognizing the same content and ID and\nreturning the original response; no retention duration is established. This\nnever permits automatic replay. A dispatched write is sent once even after\n401, timeout, server failure or an invalid success response.\n\nAfter an unknown customer-create outcome, search exact DisplayName, including\ninactive records when needed, and inspect the match. For an invoice, search\nDocNumber plus CustomerRef and inspect currency, dates, lines and computed\namounts. DocNumber can repeat, so it alone proves nothing. Reconcile and report\nambiguity before considering another create; never generate a fresh requestId\nand blindly repeat a potentially committed intent. Unknown update outcomes\nneed a read of the known Id and version. A requestId is not a lookup endpoint or\na permanent duplicate-prevention guarantee.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `quickbooks_api_details`, `quickbooks_api_search`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `quickbooks_api_search` finds this API's operations and reads no account data, `quickbooks_api_details` shows one operation's contract, and `quickbooks_api_read` calls a reviewed read. An operation without a reviewed verdict is refused. 3 operations are refused by design; use `quickbooks_api_details` with the method and path to inspect the original operation summary and its separate refusal. Use details for refused methods outside the read/write call schemas; those methods remain unavailable.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: Intuit fault 5010 for a stale SyncToken; Intuit fault 6240 for a duplicate DisplayName.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
|
+
"instructionsHeading": "Company instructions"
|
|
5
|
+
};
|