@kevludwig/eve-lexware-office 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/extension/_manifest.json +14 -0
  4. package/dist/extension/extension.d.ts +36 -0
  5. package/dist/extension/extension.mjs +36 -0
  6. package/dist/extension/hooks/approval-cards.d.ts +8 -0
  7. package/dist/extension/hooks/approval-cards.mjs +26 -0
  8. package/dist/extension/hooks/declined-reminders.d.ts +7 -0
  9. package/dist/extension/hooks/declined-reminders.mjs +27 -0
  10. package/dist/extension/instructions.md +16 -0
  11. package/dist/extension/lib/cards.d.ts +8 -0
  12. package/dist/extension/lib/cards.mjs +156 -0
  13. package/dist/extension/lib/findings.d.ts +128 -0
  14. package/dist/extension/lib/findings.mjs +176 -0
  15. package/dist/extension/lib/format.d.ts +3 -0
  16. package/dist/extension/lib/format.mjs +16 -0
  17. package/dist/extension/lib/journal.d.ts +15 -0
  18. package/dist/extension/lib/journal.mjs +27 -0
  19. package/dist/extension/lib/purchase-card.d.ts +3 -0
  20. package/dist/extension/lib/purchase-card.mjs +66 -0
  21. package/dist/extension/lib/purchase.d.ts +112 -0
  22. package/dist/extension/lib/purchase.mjs +346 -0
  23. package/dist/extension/lib/runtime.d.ts +18 -0
  24. package/dist/extension/lib/runtime.mjs +47 -0
  25. package/dist/extension/lib/sales.d.ts +72 -0
  26. package/dist/extension/lib/sales.mjs +292 -0
  27. package/dist/extension/lib/state.d.ts +9 -0
  28. package/dist/extension/lib/state.mjs +44 -0
  29. package/dist/extension/lib/types.d.ts +43 -0
  30. package/dist/extension/lib/types.mjs +7 -0
  31. package/dist/extension/skills/eingangsrechnung/SKILL.md +124 -0
  32. package/dist/extension/skills/verkaufsbelege/SKILL.md +94 -0
  33. package/dist/extension/skills/zahlungserinnerung/SKILL.md +37 -0
  34. package/dist/extension/tools/create_customer.d.ts +44 -0
  35. package/dist/extension/tools/create_customer.mjs +107 -0
  36. package/dist/extension/tools/create_invoice.d.ts +28 -0
  37. package/dist/extension/tools/create_invoice.mjs +28 -0
  38. package/dist/extension/tools/create_order_confirmation.d.ts +28 -0
  39. package/dist/extension/tools/create_order_confirmation.mjs +33 -0
  40. package/dist/extension/tools/create_purchase_invoice.d.ts +76 -0
  41. package/dist/extension/tools/create_purchase_invoice.mjs +241 -0
  42. package/dist/extension/tools/create_quotation.d.ts +28 -0
  43. package/dist/extension/tools/create_quotation.mjs +28 -0
  44. package/dist/extension/tools/due_payment_reminders.d.ts +39 -0
  45. package/dist/extension/tools/due_payment_reminders.mjs +56 -0
  46. package/dist/extension/tools/read.d.ts +10 -0
  47. package/dist/extension/tools/read.mjs +83 -0
  48. package/dist/extension/tools/send_payment_reminder.d.ts +57 -0
  49. package/dist/extension/tools/send_payment_reminder.mjs +180 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.mjs +8 -0
  52. package/dist/tools/index.d.ts +10 -0
  53. package/dist/tools/index.mjs +15 -0
  54. package/package.json +69 -0
@@ -0,0 +1,176 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { eur, shortDate } from "./format.mjs";
8
+ import { round2 } from "@kevludwig/lexware-office";
9
+ import { defineState } from "eve/context";
10
+ const warnings = defineState("warnings", () => ({}));
11
+ const rejected = defineState("rejected-signatures", () => ({}));
12
+ function saveWarnings(callId, list) {
13
+ try {
14
+ warnings.update((all) => ({
15
+ ...all,
16
+ [callId]: list
17
+ }));
18
+ } catch (error) {
19
+ console.warn(`[lexware] Cannot store findings: ${String(error)}`);
20
+ }
21
+ }
22
+ function warningsOf(callId) {
23
+ try {
24
+ return warnings.get()[callId] ?? [];
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+ function findingsOf(callId) {
30
+ return warningsOf(callId).map(describeWarning);
31
+ }
32
+ function notesOf(callId) {
33
+ return findingsOf(callId).map((finding) => `${finding.title}: ${finding.value}`);
34
+ }
35
+ function wasShownOnCard(callId, url) {
36
+ return warningsOf(callId).some((warning) => (warning.kind === "sales-duplicate" || warning.kind === "voucher-duplicate") && warning.url === url);
37
+ }
38
+ function isCorrectable(warning) {
39
+ return warning.kind === "voucher-total-mismatch" || warning.kind === "category-ambiguous" || warning.kind === "category-no-history" || warning.kind === "category-split" || warning.kind === "category-deviation";
40
+ }
41
+ function wasRejected(signature) {
42
+ try {
43
+ return rejected.get()[signature] === true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ function rememberRejection(signature) {
49
+ try {
50
+ rejected.update((all) => ({
51
+ ...all,
52
+ [signature]: true
53
+ }));
54
+ } catch {}
55
+ }
56
+ const amountIn = (value, currency) => eur(value, currency);
57
+ function describeWarning(warning) {
58
+ switch (warning.kind) {
59
+ case "note": return {
60
+ title: warning.title,
61
+ value: warning.value
62
+ };
63
+ case "sales-duplicate": return {
64
+ title: "⚠ Dublette",
65
+ value: `${warning.documentLabel} mit gleicher Positionssumme existiert bereits` + (warning.voucherNumber ? ` (${warning.voucherNumber}` : " (") + (warning.voucherDate ? `, ${shortDate(warning.voucherDate)}` : "") + (warning.voucherStatus ? `, ${warning.voucherStatus}` : "") + `): ${warning.url}`
66
+ };
67
+ case "sales-duplicate-check-failed": return {
68
+ title: "⚠ Dublettenprüfung",
69
+ value: `fehlgeschlagen — ${warning.reason}`
70
+ };
71
+ case "duplicate-check-incomplete": return {
72
+ title: "⚠ Dublettenprüfung",
73
+ value: `unvollständig — ${warning.reason}`
74
+ };
75
+ case "sales-contact": return {
76
+ title: "⚠ Kunde",
77
+ value: warning.reason
78
+ };
79
+ case "customer-resolved": return {
80
+ title: "Kunde",
81
+ value: warning.number !== void 0 ? `${warning.name} (Nr. ${warning.number})` : warning.name
82
+ };
83
+ case "source-resolved": return {
84
+ title: "Bezugsbeleg",
85
+ value: warning.contactName ? `${warning.sourceLabel} (${warning.contactName})` : warning.sourceLabel
86
+ };
87
+ case "source-mismatch": return {
88
+ title: "⚠ Bezugsbeleg",
89
+ value: `Positionen ergeben ${eur(warning.itemsNet)}, ${warning.sourceLabel} hat ${eur(warning.sourceNet)} (Differenz ${eur(warning.difference)}) — Abweichung kann gewollt sein (geänderte Positionen)`
90
+ };
91
+ case "source-discount": {
92
+ const parts = [warning.discountAbsolute ? eur(warning.discountAbsolute) : null, warning.discountPercentage ? `${warning.discountPercentage} %` : null].filter(Boolean);
93
+ return {
94
+ title: "⚠ Rabatt im Bezugsbeleg",
95
+ value: `${warning.sourceLabel} enthält einen Gesamtrabatt (${parts.join(" / ")})` + (warning.sourceNet !== void 0 ? `, End-Netto ${eur(warning.sourceNet)}` : "") + " — der neue Beleg übernimmt ihn NICHT; ohne eigenen Rabatt entsteht er zum vollen Preis"
96
+ };
97
+ }
98
+ case "source-check-failed": return {
99
+ title: "⚠ Bezugsbeleg",
100
+ value: `nicht prüfbar — ${warning.reason}`
101
+ };
102
+ case "voucher-total-mismatch": return {
103
+ title: "⚠ Summenabgleich",
104
+ value: `Steuergruppen ergeben ${amountIn(warning.computed, warning.currency)} brutto, angegeben ist ${amountIn(warning.expected, warning.currency)} (Differenz ${amountIn(warning.difference, warning.currency)})`
105
+ };
106
+ case "voucher-reverse-charge": {
107
+ const booking = `${warning.categories.join(", ")}: gebucht mit ${warning.rate} % ohne Steuerbetrag — die Umsatzsteuer schuldest du selbst und ziehst sie als Vorsteuer wieder ab`;
108
+ return warning.invoiceChargesTax ? {
109
+ title: "⚠ Reverse Charge",
110
+ value: `${booking}. Die Rechnung weist aber Umsatzsteuer aus — Kategorie oder Rechnung prüfen`
111
+ } : {
112
+ title: "Reverse Charge",
113
+ value: booking
114
+ };
115
+ }
116
+ case "voucher-foreign-currency": {
117
+ const rate = warning.rate.toLocaleString("de-DE", {
118
+ minimumFractionDigits: 4,
119
+ maximumFractionDigits: 4
120
+ });
121
+ const conversion = `${amountIn(warning.original, warning.currency)} laut Rechnung, gebucht mit ${eur(warning.booked)} laut Kontoauszug (1 ${warning.currency} = ${rate} EUR)`;
122
+ return round2(warning.booked) === round2(warning.original) ? {
123
+ title: "⚠ Fremdwährung",
124
+ value: `${conversion} — Kurs 1:1, vermutlich nicht umgerechnet`
125
+ } : {
126
+ title: "Fremdwährung",
127
+ value: conversion
128
+ };
129
+ }
130
+ case "voucher-duplicate": return {
131
+ title: "⚠ Dublette",
132
+ value: `Rechnung ${warning.voucherNumber} ist bereits als Eingangsbeleg erfasst` + (warning.contactName ? ` (${warning.contactName}` : " (") + (warning.voucherDate ? `, ${shortDate(warning.voucherDate)}` : "") + (warning.voucherStatus ? `, ${warning.voucherStatus}` : "") + `): ${warning.url}`
133
+ };
134
+ case "voucher-duplicate-check-failed": return {
135
+ title: "⚠ Dublettenprüfung",
136
+ value: `fehlgeschlagen — ${warning.reason}`
137
+ };
138
+ case "voucher-category-missing": return {
139
+ title: "⚠ Buchungskategorie",
140
+ value: warning.reason
141
+ };
142
+ case "voucher-attachment-missing": return {
143
+ title: "⚠ Anhang",
144
+ value: warning.reason
145
+ };
146
+ case "voucher-contact-fallback": return {
147
+ title: "⚠ Lieferant",
148
+ value: warning.reason
149
+ };
150
+ case "voucher-contact-explicit": return {
151
+ title: "⚠ Kontakt",
152
+ value: `Beleg geht per contact_id an „${warning.contactName}" (${warning.contactId}) — auf der Rechnung steht „${warning.extractedName}"`
153
+ };
154
+ case "category-history": return {
155
+ title: "Buchungskategorie",
156
+ value: `${warning.name} (${warning.basis})`
157
+ };
158
+ case "category-ambiguous": return {
159
+ title: "⚠ Buchungskategorie",
160
+ value: `dieser Lieferant wurde bisher auf mehrere Kategorien gebucht (${warning.candidates.join(", ")}) — wähle anhand des Dokumentinhalts eine davon und übergib sie als category; ist der Inhalt nicht eindeutig, frage den Nutzer. Ohne Wahl wird der Beleg ohne Kategorie erfasst`
161
+ };
162
+ case "category-no-history": return {
163
+ title: "⚠ Buchungskategorie",
164
+ value: "keine gebuchten Belege dieses Lieferanten als Vorlage — sieh mit dem Lese-Tool unter /posting-categories nach, wähle eine fachlich passende Kategorie und übergib sie als category; bei Unsicherheit frage den Nutzer. Ohne Wahl wird der Beleg ohne Kategorie erfasst"
165
+ };
166
+ case "category-split": return {
167
+ title: "⚠ Buchungskategorie",
168
+ value: `gebuchte Belege dieses Lieferanten sind innerhalb eines Belegs auf mehrere Kategorien aufgeteilt (${warning.candidates.join(", ")}) — eine Einzelwahl wäre falsch. Teile den Beleg anhand des Dokumentinhalts auf: eine tax_group je Kategorie (auch bei gleichem Steuersatz), jeweils mit category. Ist die Aufteilung nicht ableitbar, frage den Nutzer; ohne Aufteilung wird der Beleg ohne Kategorie erfasst`
169
+ };
170
+ case "category-deviation": return {
171
+ title: "⚠ Buchungskategorie",
172
+ value: `„${warning.chosen}" weicht von der bisher üblichen Kategorie dieses Lieferanten ab („${warning.usual}")`
173
+ };
174
+ }
175
+ }
176
+ export { describeWarning, findingsOf, isCorrectable, notesOf, rememberRejection, saveWarnings, warningsOf, wasRejected, wasShownOnCard };
@@ -0,0 +1,3 @@
1
+ /** How amounts and dates read on cards and in answers: „761,60 EUR", „1.9.2026". */
2
+ export declare function eur(value: number, currency?: string): string;
3
+ export declare function shortDate(value: string): string;
@@ -0,0 +1,16 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ function eur(value, currency = "EUR") {
8
+ return `${value.toLocaleString("de-DE", {
9
+ minimumFractionDigits: 2,
10
+ maximumFractionDigits: 2
11
+ })} ${currency}`;
12
+ }
13
+ function shortDate(value) {
14
+ return new Date(/^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T12:00:00Z` : value).toLocaleDateString("de-DE", { timeZone: "Europe/Berlin" });
15
+ }
16
+ export { eur, shortDate };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Idempotency journal, per tool call. Tools run as durable steps and may run
3
+ * again after a crash — even after a successful write, if the step's commit
4
+ * was lost. `pending` before the write, `created` after it: a replay finds
5
+ * the entry and does not write twice.
6
+ *
7
+ * Never throws: an unwritable journal must not block an approved action.
8
+ */
9
+ export interface JournalEntry {
10
+ status: "pending" | "created" | "attached";
11
+ resourceId?: string;
12
+ at: string;
13
+ }
14
+ export declare function readJournal(kind: string, callId: string): Promise<JournalEntry | null>;
15
+ export declare function writeJournal(kind: string, callId: string, entry: Omit<JournalEntry, "at">): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { key, store } from "./runtime.mjs";
8
+ async function readJournal(kind, callId) {
9
+ try {
10
+ const entry = await store().read(key("journal", kind, callId));
11
+ return entry && typeof entry.status === "string" ? entry : null;
12
+ } catch (error) {
13
+ console.warn(`[lexware] Cannot read journal ${kind}/${callId}: ${String(error)}`);
14
+ return null;
15
+ }
16
+ }
17
+ async function writeJournal(kind, callId, entry) {
18
+ try {
19
+ await store().write(key("journal", kind, callId), {
20
+ ...entry,
21
+ at: new Date().toISOString()
22
+ });
23
+ } catch (error) {
24
+ console.warn(`[lexware] Cannot write journal ${kind}/${callId}: ${String(error)}`);
25
+ }
26
+ }
27
+ export { readJournal, writeJournal };
@@ -0,0 +1,3 @@
1
+ /** The purchase invoice on the approval card — from the tool input; the findings come from the checks. */
2
+ import type { ApprovalCard } from "./types";
3
+ export declare function purchaseCard(raw: Record<string, unknown>): Omit<ApprovalCard, "tool" | "findings">;
@@ -0,0 +1,66 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { eur } from "./format.mjs";
8
+ import { bookedGroupsOf, invoiceCurrencyOf, isForeignCurrency, totalsOf } from "./purchase.mjs";
9
+ const basename = (path) => path.split("/").pop() || path;
10
+ function purchaseCard(raw) {
11
+ const input = raw;
12
+ const groups = bookedGroupsOf(input);
13
+ const totals = totalsOf(groups);
14
+ const currency = invoiceCurrencyOf(input);
15
+ const invoiceGross = totalsOf(input.tax_groups).gross;
16
+ const supplier = input.supplier_name?.trim() || "unbekannter Lieferant";
17
+ const number = input.voucher_number?.trim() || "ohne Nummer";
18
+ const facts = [{
19
+ title: "Lieferant",
20
+ value: supplier
21
+ }, {
22
+ title: "Rechnungsnummer",
23
+ value: number
24
+ }];
25
+ if (input.voucher_date) facts.push({
26
+ title: "Belegdatum",
27
+ value: input.voucher_date
28
+ });
29
+ if (groups.length > 0) {
30
+ facts.push({
31
+ title: "Brutto",
32
+ value: eur(totals.gross)
33
+ });
34
+ facts.push({
35
+ title: "davon USt.",
36
+ value: `${eur(totals.tax)} (${groups.map((group) => `${eur(group.gross_amount ?? 0)} zu ${group.tax_rate_percent ?? 0} %`).join(", ")})`
37
+ });
38
+ facts.push({
39
+ title: "Netto",
40
+ value: eur(totals.net)
41
+ });
42
+ }
43
+ if (typeof input.total_gross_amount === "number") {
44
+ const difference = Math.round((invoiceGross - input.total_gross_amount) * 100) / 100;
45
+ facts.push({
46
+ title: "Rechnungsendbetrag",
47
+ value: difference === 0 ? `${eur(input.total_gross_amount, currency)} — passt` : `${eur(input.total_gross_amount, currency)} — Abweichung ${eur(difference, currency)}`
48
+ });
49
+ }
50
+ const groupCategories = groups.map((group) => group.category?.trim()).filter((name) => Boolean(name));
51
+ facts.push({
52
+ title: "Buchungskategorie",
53
+ value: groupCategories.length > 0 ? groupCategories.map((name, index) => `${name} (${eur(groups[index]?.gross_amount ?? 0)})`).join(", ") : input.category?.trim() || "automatisch nach Lieferanten-Historie — siehe Befunde"
54
+ });
55
+ facts.push({
56
+ title: "Original",
57
+ value: input.attachment_path ? basename(input.attachment_path) : "kein Anhang"
58
+ });
59
+ return {
60
+ title: "Eingangsrechnung erfassen?",
61
+ subtitle: `${supplier} · Rechnung ${number} · ${eur(totals.gross)} brutto` + (isForeignCurrency(input) ? ` (${eur(invoiceGross, currency)})` : ""),
62
+ facts,
63
+ note: "Wird als Beleg zur Prüfung angelegt und in Lexware Office noch bestätigt."
64
+ };
65
+ }
66
+ export { purchaseCard };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The purchase invoice: foreign currency, supplier, posting category, and the
3
+ * checks before the card.
4
+ *
5
+ * Lexware Office books in EUR. For an invoice in another currency the right
6
+ * amount is the one actually debited — fees and the bank's rate included —
7
+ * so the user supplies it from the bank statement and the conversion happens
8
+ * here, deterministically; the totals check stays in the invoice currency.
9
+ */
10
+ import { type PostingCategory } from "@kevludwig/lexware-office";
11
+ import type { Warning } from "./findings";
12
+ export declare const BOOKING_CURRENCY = "EUR";
13
+ export interface TaxGroupInput {
14
+ gross_amount?: number;
15
+ tax_rate_percent?: number;
16
+ /** Category of this group — only for a voucher split across categories. */
17
+ category?: string;
18
+ }
19
+ export interface PurchaseInput {
20
+ supplier_name?: string;
21
+ voucher_number?: string;
22
+ voucher_date?: string;
23
+ due_date?: string;
24
+ currency?: string;
25
+ bank_statement_eur_amount?: number;
26
+ total_gross_amount?: number;
27
+ tax_groups?: readonly TaxGroupInput[];
28
+ category?: string;
29
+ contact_id?: string;
30
+ remark?: string;
31
+ attachment_path?: string;
32
+ }
33
+ export declare const MISSING_EUR_AMOUNT_INSTRUCTION: string;
34
+ export declare const invoiceCurrencyOf: (input: PurchaseInput) => string;
35
+ export declare const isForeignCurrency: (input: PurchaseInput) => boolean;
36
+ export declare const lacksEurAmount: (input: PurchaseInput) => boolean;
37
+ /**
38
+ * The tax groups as they are booked: in EUR. A foreign invoice's groups are
39
+ * scaled to the debited amount; the last group takes the rounding remainder,
40
+ * so the groups meet the bank line to the cent.
41
+ */
42
+ export declare function bookedGroupsOf<G extends TaxGroupInput>(input: PurchaseInput & {
43
+ tax_groups?: readonly G[];
44
+ }): G[];
45
+ /** EUR per unit of the invoice currency, implied by the debit — shown on the card. Null for EUR. */
46
+ export declare function impliedRateOf(input: PurchaseInput): number | null;
47
+ export declare function totalsOf(groups?: readonly TaxGroupInput[]): {
48
+ gross: number;
49
+ tax: number;
50
+ net: number;
51
+ };
52
+ export declare const isAllowedAttachmentPath: (path: string) => boolean;
53
+ export interface SupplierTarget {
54
+ contactId?: string;
55
+ /** The name on the voucher — also for the collective vendor. */
56
+ contactName: string;
57
+ warnings: Warning[];
58
+ }
59
+ /**
60
+ * The vendor contact for the name on the invoice. The collective vendor is
61
+ * the fallback — and always a finding, so a voucher never lands there
62
+ * silently. A passed contact_id is verified, and the card carries the
63
+ * contact's real name.
64
+ */
65
+ export declare function resolveSupplier(input: PurchaseInput, signal?: AbortSignal): Promise<SupplierTarget>;
66
+ /** Supplier and categories the card showed, bound to the call — execute writes exactly that. */
67
+ export interface ApprovedPurchaseTarget {
68
+ contactId?: string;
69
+ contactName: string;
70
+ categoryId?: string;
71
+ categoryName?: string;
72
+ /** Per-group categories of a split voucher, aligned with tax_groups. */
73
+ groupCategories?: Array<{
74
+ id?: string;
75
+ name?: string;
76
+ }>;
77
+ }
78
+ export declare function rememberPurchaseTarget(callId: string, target: ApprovedPurchaseTarget): void;
79
+ export declare function approvedPurchaseTarget(callId: string): ApprovedPurchaseTarget | null;
80
+ /** One concrete capture — same numbers and same category choice: each new choice runs the correction cycle once. */
81
+ export declare function purchaseSignatureOf(input: PurchaseInput): string;
82
+ /**
83
+ * Totals, currency, supplier, category, reverse charge, duplicate, and
84
+ * attachment. Network errors become findings. Returns supplier and category
85
+ * too, so the approval can bind them to the call.
86
+ */
87
+ export declare function runPurchasePreflight(input: PurchaseInput, options?: {
88
+ signal?: AbortSignal;
89
+ readFileSize?: (path: string) => Promise<number | null>;
90
+ }): Promise<{
91
+ warnings: Warning[];
92
+ supplier: SupplierTarget;
93
+ category: PostingCategory | null;
94
+ groupCategories: Array<PostingCategory | null> | null;
95
+ }>;
96
+ /** Per-group categories when a group names one, otherwise the single pick. */
97
+ export declare function resolveCategories(input: PurchaseInput, contactId: string | undefined, signal?: AbortSignal): Promise<{
98
+ category: PostingCategory | null;
99
+ groupCategories: Array<PostingCategory | null> | null;
100
+ warnings: Warning[];
101
+ }>;
102
+ /**
103
+ * The posting category, in this order: the one the call names (checked
104
+ * against the account, a deviation from the vendor's usual one is a finding);
105
+ * the vendor's history — one category is taken over, several are for the
106
+ * model to choose from; no history — the model looks one up or asks. A
107
+ * vendor whose vouchers are split within one voucher gets no single pick.
108
+ */
109
+ export declare function pickCategory(input: PurchaseInput, contactId: string | undefined, signal?: AbortSignal): Promise<{
110
+ category: PostingCategory | null;
111
+ warnings: Warning[];
112
+ }>;