@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,346 @@
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 { client } from "./runtime.mjs";
8
+ import { ITEMS_TOTAL_TOLERANCE, REVERSE_CHARGE_TAX_RATE, describeError, findPurchaseInvoiceByNumber, findVendorCategoryHistory, getContact, isReverseChargeCategory, resolveCategory, resolveContactByName, round2, taxFromGross } from "@kevludwig/lexware-office";
9
+ import { defineState } from "eve/context";
10
+ const BOOKING_CURRENCY = "EUR";
11
+ const MISSING_EUR_AMOUNT_INSTRUCTION = "Die Rechnung ist nicht in EUR. Lexware Office bucht in EUR, maßgeblich ist der tatsächlich abgebuchte Betrag. Frage den Nutzer (Freitext) nach dem EUR-Betrag laut Kontoauszug — nicht selbst umrechnen, keinen Kurs schätzen — und rufe das Tool dann mit bank_statement_eur_amount erneut auf. Beträge und tax_groups bleiben in Rechnungswährung.";
12
+ const invoiceCurrencyOf = (input) => input.currency?.trim().toUpperCase() || "EUR";
13
+ const isForeignCurrency = (input) => invoiceCurrencyOf(input) !== "EUR";
14
+ const lacksEurAmount = (input) => isForeignCurrency(input) && !(typeof input.bank_statement_eur_amount === "number" && input.bank_statement_eur_amount > 0);
15
+ function bookedGroupsOf(input) {
16
+ const groups = [...input.tax_groups ?? []];
17
+ if (!isForeignCurrency(input) || lacksEurAmount(input)) return groups;
18
+ const target = round2(input.bank_statement_eur_amount);
19
+ const original = groups.reduce((sum, group) => sum + (group.gross_amount ?? 0), 0);
20
+ if (original <= 0) return groups;
21
+ let assigned = 0;
22
+ return groups.map((group, index) => {
23
+ const amount = index === groups.length - 1 ? round2(target - assigned) : round2((group.gross_amount ?? 0) * target / original);
24
+ assigned = round2(assigned + amount);
25
+ return {
26
+ ...group,
27
+ gross_amount: amount
28
+ };
29
+ });
30
+ }
31
+ function impliedRateOf(input) {
32
+ if (!isForeignCurrency(input) || lacksEurAmount(input)) return null;
33
+ const original = (input.tax_groups ?? []).reduce((sum, group) => sum + (group.gross_amount ?? 0), 0);
34
+ return original > 0 ? input.bank_statement_eur_amount / original : null;
35
+ }
36
+ function totalsOf(groups = []) {
37
+ let gross = 0;
38
+ let tax = 0;
39
+ for (const group of groups) {
40
+ gross += group.gross_amount ?? 0;
41
+ tax += taxFromGross(group.gross_amount ?? 0, group.tax_rate_percent ?? 0);
42
+ }
43
+ return {
44
+ gross: round2(gross),
45
+ tax: round2(tax),
46
+ net: round2(gross - tax)
47
+ };
48
+ }
49
+ const ATTACHMENT_PATH = /^\/workspace\/attachments\/[0-9a-f]{8,64}\/[\w.-]+$/;
50
+ const isAllowedAttachmentPath = (path) => ATTACHMENT_PATH.test(path);
51
+ async function resolveSupplier(input, signal) {
52
+ const name = input.supplier_name?.trim() || "unbekannter Lieferant";
53
+ const fallback = (reason) => ({
54
+ contactName: name,
55
+ warnings: [{
56
+ kind: "voucher-contact-fallback",
57
+ reason
58
+ }]
59
+ });
60
+ if (input.contact_id) try {
61
+ const contact = await getContact(client(), input.contact_id, signal);
62
+ if (!contact.isVendor) return fallback(`contact_id ${input.contact_id} („${contact.name}") ist kein Lieferantenkontakt — der Beleg geht an den Sammellieferanten`);
63
+ return {
64
+ contactId: contact.id,
65
+ contactName: contact.name,
66
+ warnings: contact.name.toLowerCase() === name.toLowerCase() ? [] : [{
67
+ kind: "voucher-contact-explicit",
68
+ contactName: contact.name,
69
+ contactId: contact.id,
70
+ extractedName: name
71
+ }]
72
+ };
73
+ } catch (error) {
74
+ return fallback(`contact_id ${input.contact_id} ließ sich nicht verifizieren (${describeError(error)}) — der Beleg geht an den Sammellieferanten`);
75
+ }
76
+ try {
77
+ const { match, candidates } = await resolveContactByName(client(), name, {
78
+ vendor: true,
79
+ signal
80
+ });
81
+ if (match) return {
82
+ contactId: match.id,
83
+ contactName: match.name,
84
+ warnings: []
85
+ };
86
+ if (candidates.length > 1) return fallback(`„${name}" passt auf ${candidates.length} Lieferantenkontakte (${candidates.map((contact) => contact.name).join(", ")}) — der Beleg geht an den Sammellieferanten. Mit contact_id lässt sich einer davon festlegen.`);
87
+ return fallback(`kein Lieferantenkontakt „${name}" — der Beleg geht an den Sammellieferanten`);
88
+ } catch (error) {
89
+ return fallback(`Kontaktsuche fehlgeschlagen (${describeError(error)}) — der Beleg geht an den Sammellieferanten`);
90
+ }
91
+ }
92
+ const approvedTargets = defineState("purchase-targets", () => ({}));
93
+ function rememberPurchaseTarget(callId, target) {
94
+ try {
95
+ approvedTargets.update((all) => ({
96
+ ...all,
97
+ [callId]: target
98
+ }));
99
+ } catch (error) {
100
+ console.warn(`[lexware] Cannot bind purchase target: ${String(error)}`);
101
+ }
102
+ }
103
+ function approvedPurchaseTarget(callId) {
104
+ try {
105
+ return approvedTargets.get()[callId] ?? null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ function purchaseSignatureOf(input) {
111
+ const groups = input.tax_groups ?? [];
112
+ return [
113
+ "voucher",
114
+ input.voucher_number?.trim() ?? "-",
115
+ input.voucher_date ?? "-",
116
+ invoiceCurrencyOf(input),
117
+ (input.bank_statement_eur_amount ?? 0).toFixed(2),
118
+ (input.total_gross_amount ?? 0).toFixed(2),
119
+ groups.length,
120
+ totalsOf(groups).gross.toFixed(2),
121
+ input.category?.trim().toLowerCase() ?? "-",
122
+ groups.map((group) => group.category?.trim().toLowerCase() ?? "").join(",")
123
+ ].join("|");
124
+ }
125
+ async function runPurchasePreflight(input, options = {}) {
126
+ const warnings = [];
127
+ const groups = input.tax_groups ?? [];
128
+ if (groups.length > 0 && typeof input.total_gross_amount === "number") {
129
+ const computed = totalsOf(groups).gross;
130
+ const difference = round2(computed - input.total_gross_amount);
131
+ if (Math.abs(difference) > ITEMS_TOTAL_TOLERANCE) warnings.push({
132
+ kind: "voucher-total-mismatch",
133
+ computed,
134
+ expected: input.total_gross_amount,
135
+ difference,
136
+ currency: invoiceCurrencyOf(input)
137
+ });
138
+ }
139
+ const rate = impliedRateOf(input);
140
+ if (rate !== null) warnings.push({
141
+ kind: "voucher-foreign-currency",
142
+ currency: invoiceCurrencyOf(input),
143
+ original: totalsOf(groups).gross,
144
+ booked: round2(input.bank_statement_eur_amount),
145
+ rate
146
+ });
147
+ const supplier = await resolveSupplier(input, options.signal);
148
+ warnings.push(...supplier.warnings);
149
+ const picked = await resolveCategories(input, supplier.contactId, options.signal);
150
+ warnings.push(...picked.warnings);
151
+ warnings.push(...reverseChargeFindings(groups, picked));
152
+ warnings.push(...await checkDuplicate(input, options.signal));
153
+ warnings.push(...await checkAttachment(input, options.readFileSize));
154
+ return {
155
+ warnings,
156
+ supplier,
157
+ category: picked.category,
158
+ groupCategories: picked.groupCategories
159
+ };
160
+ }
161
+ function reverseChargeFindings(groups, picked) {
162
+ const nameOf = (index) => picked.groupCategories ? picked.groupCategories[index]?.name : picked.category?.name;
163
+ const affected = groups.map((group, index) => ({
164
+ group,
165
+ name: nameOf(index)
166
+ })).filter((entry) => isReverseChargeCategory(entry.name));
167
+ if (affected.length === 0) return [];
168
+ return [{
169
+ kind: "voucher-reverse-charge",
170
+ categories: [...new Set(affected.map((entry) => entry.name))],
171
+ rate: REVERSE_CHARGE_TAX_RATE,
172
+ invoiceChargesTax: affected.some((entry) => (entry.group.tax_rate_percent ?? 0) > 0)
173
+ }];
174
+ }
175
+ async function resolveCategories(input, contactId, signal) {
176
+ const groups = input.tax_groups ?? [];
177
+ if (!groups.some((group) => group.category?.trim())) return {
178
+ ...await pickCategory(input, contactId, signal),
179
+ groupCategories: null
180
+ };
181
+ const warnings = [];
182
+ const resolved = [];
183
+ for (const [index, group] of groups.entries()) {
184
+ const name = group.category?.trim();
185
+ if (!name) {
186
+ resolved.push(null);
187
+ continue;
188
+ }
189
+ try {
190
+ const match = await resolveCategory(client(), name, "outgo", signal);
191
+ if (!match) warnings.push({
192
+ kind: "voucher-category-missing",
193
+ reason: `„${name}" (Gruppe ${index + 1}) ist keine Ausgabenkategorie dieses Kontos — Name wörtlich aus /posting-categories übernehmen; die Gruppe wird sonst ohne Kategorie erfasst`
194
+ });
195
+ resolved.push(match);
196
+ } catch (error) {
197
+ warnings.push({
198
+ kind: "voucher-category-missing",
199
+ reason: `Kategorien nicht abrufbar (${describeError(error)}) — Gruppe ${index + 1} wird ohne Kategorie erfasst`
200
+ });
201
+ resolved.push(null);
202
+ }
203
+ }
204
+ return {
205
+ category: null,
206
+ groupCategories: resolved,
207
+ warnings
208
+ };
209
+ }
210
+ async function pickCategory(input, contactId, signal) {
211
+ const requested = input.category?.trim();
212
+ const warnings = [];
213
+ let history = null;
214
+ let historyError = null;
215
+ if (contactId) try {
216
+ history = await findVendorCategoryHistory(client(), contactId, { signal });
217
+ } catch (error) {
218
+ historyError = describeError(error);
219
+ }
220
+ try {
221
+ if (requested) {
222
+ const match = await resolveCategory(client(), requested, "outgo", signal);
223
+ if (match) {
224
+ if (history?.split) warnings.push({
225
+ kind: "category-split",
226
+ candidates: history.categories.map((category) => category.name)
227
+ });
228
+ const usual = history?.categories ?? [];
229
+ if (usual.length > 0 && !usual.some((category) => category.id === match.id)) warnings.push({
230
+ kind: "category-deviation",
231
+ chosen: match.name,
232
+ usual: usual.map((category) => category.name).join(", ")
233
+ });
234
+ return {
235
+ category: match,
236
+ warnings
237
+ };
238
+ }
239
+ warnings.push({
240
+ kind: "voucher-category-missing",
241
+ reason: `„${requested}" ist keine Ausgabenkategorie dieses Kontos — Name wörtlich aus /posting-categories übernehmen`
242
+ });
243
+ }
244
+ if (history) {
245
+ if (history.split) {
246
+ warnings.push({
247
+ kind: "category-split",
248
+ candidates: history.categories.map((category) => category.name)
249
+ });
250
+ return {
251
+ category: null,
252
+ warnings
253
+ };
254
+ }
255
+ if (history.categories.length === 1) {
256
+ const match = await resolveCategory(client(), history.categories[0].id, "outgo", signal);
257
+ if (match) {
258
+ warnings.push({
259
+ kind: "category-history",
260
+ name: match.name,
261
+ basis: `wie die ${history.scanned === 1 ? "letzte gebuchte Rechnung" : `letzten ${history.scanned} gebuchten Rechnungen`} dieses Lieferanten` + (history.incomplete ? " (ältere ungeprüft)" : "")
262
+ });
263
+ return {
264
+ category: match,
265
+ warnings
266
+ };
267
+ }
268
+ }
269
+ if (history.categories.length > 1) {
270
+ warnings.push({
271
+ kind: "category-ambiguous",
272
+ candidates: history.categories.map((category) => category.name)
273
+ });
274
+ return {
275
+ category: null,
276
+ warnings
277
+ };
278
+ }
279
+ }
280
+ if (historyError) {
281
+ warnings.push({
282
+ kind: "voucher-category-missing",
283
+ reason: `Lieferanten-Historie nicht abrufbar (${historyError}) — der Beleg wird ohne Kategorie erfasst`
284
+ });
285
+ return {
286
+ category: null,
287
+ warnings
288
+ };
289
+ }
290
+ warnings.push({ kind: "category-no-history" });
291
+ return {
292
+ category: null,
293
+ warnings
294
+ };
295
+ } catch (error) {
296
+ warnings.push({
297
+ kind: "voucher-category-missing",
298
+ reason: `Kategorien nicht abrufbar (${describeError(error)}) — der Beleg wird ohne Kategorie erfasst`
299
+ });
300
+ return {
301
+ category: null,
302
+ warnings
303
+ };
304
+ }
305
+ }
306
+ async function checkDuplicate(input, signal) {
307
+ const number = input.voucher_number?.trim();
308
+ if (!number) return [];
309
+ try {
310
+ const existing = await findPurchaseInvoiceByNumber(client(), number, signal);
311
+ return existing ? [{
312
+ kind: "voucher-duplicate",
313
+ url: existing.url,
314
+ voucherNumber: existing.voucherNumber ?? number,
315
+ voucherDate: existing.voucherDate,
316
+ voucherStatus: existing.voucherStatus,
317
+ contactName: existing.contactName
318
+ }] : [];
319
+ } catch (error) {
320
+ return [{
321
+ kind: "voucher-duplicate-check-failed",
322
+ reason: describeError(error)
323
+ }];
324
+ }
325
+ }
326
+ async function checkAttachment(input, readFileSize) {
327
+ const path = input.attachment_path?.trim();
328
+ if (!path) return [];
329
+ if (!isAllowedAttachmentPath(path)) return [{
330
+ kind: "voucher-attachment-missing",
331
+ reason: `„${path}" liegt nicht unter /workspace/attachments/ — es wird kein Original angehängt`
332
+ }];
333
+ if (!readFileSize) return [];
334
+ try {
335
+ return await readFileSize(path) === null ? [{
336
+ kind: "voucher-attachment-missing",
337
+ reason: `${path} existiert nicht — es wird kein Original angehängt`
338
+ }] : [];
339
+ } catch (error) {
340
+ return [{
341
+ kind: "voucher-attachment-missing",
342
+ reason: `${path} nicht lesbar (${describeError(error)})`
343
+ }];
344
+ }
345
+ }
346
+ export { BOOKING_CURRENCY, MISSING_EUR_AMOUNT_INSTRUCTION, approvedPurchaseTarget, bookedGroupsOf, impliedRateOf, invoiceCurrencyOf, isAllowedAttachmentPath, isForeignCurrency, lacksEurAmount, pickCategory, purchaseSignatureOf, rememberPurchaseTarget, resolveCategories, resolveSupplier, runPurchasePreflight, totalsOf };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The mount's configuration turned into working parts — one API client (one
3
+ * rate-limit queue) and one store per mount, created on first use.
4
+ */
5
+ import { type JsonStore, type LexwareClient, type ReminderLedger } from "@kevludwig/lexware-office";
6
+ import type { ApprovalResponseContext, ApprovalResponseDecision } from "eve/tools/approval";
7
+ import extension from "../extension";
8
+ type Config = typeof extension.config;
9
+ export declare const config: () => Config;
10
+ export declare const client: () => LexwareClient;
11
+ export declare const store: () => JsonStore;
12
+ /** A key of this mount's own documents in the store, e.g. the journal. */
13
+ export declare function key(...segments: string[]): string;
14
+ export declare function ledger(): ReminderLedger;
15
+ export declare function renderReminder(): import("@kevludwig/lexware-office").RenderReminderMail;
16
+ /** The response policy of every writing tool: the mount's canApprove, if any. */
17
+ export declare function approverPolicy<T>({ responder }: ApprovalResponseContext<T>): Promise<ApprovalResponseDecision>;
18
+ export {};
@@ -0,0 +1,47 @@
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 extension_default from "../extension.mjs";
8
+ import { createLexwareClient, defaultReminderMail, keySegment, reminderLedger } from "@kevludwig/lexware-office";
9
+ import { vercelBlobStore } from "@kevludwig/lexware-office/vercel-blob";
10
+ let cached;
11
+ function parts() {
12
+ const config = extension_default.config;
13
+ if (cached?.config !== config) cached = {
14
+ config,
15
+ client: config.client ?? createLexwareClient({
16
+ apiKey: config.apiKey ?? "",
17
+ baseUrl: config.baseUrl,
18
+ appUrl: config.appUrl
19
+ }),
20
+ store: config.storage.store ?? vercelBlobStore({ prefix: config.storage.prefix })
21
+ };
22
+ return cached;
23
+ }
24
+ const config = () => extension_default.config;
25
+ const client = () => parts().client;
26
+ const store = () => parts().store;
27
+ function key(...segments) {
28
+ return segments.map(keySegment).join("/");
29
+ }
30
+ function ledger() {
31
+ const reminders = config().reminders;
32
+ return reminderLedger(store(), reminders.namespace ?? `reminders/${reminders.mode}`);
33
+ }
34
+ function renderReminder() {
35
+ const reminders = config().reminders;
36
+ if (reminders.render) return reminders.render;
37
+ return defaultReminderMail(reminders.sender ?? { name: "Ihr Team" });
38
+ }
39
+ async function approverPolicy({ responder }) {
40
+ const canApprove = config().canApprove;
41
+ if (!canApprove || await canApprove(responder)) return { status: "allowed" };
42
+ return {
43
+ status: "rejected",
44
+ reason: "Nur berechtigte Personen dürfen freigeben."
45
+ };
46
+ }
47
+ export { approverPolicy, client, config, key, ledger, renderReminder, store };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * What quotation, order confirmation, and invoice share: the line-item
3
+ * schema, finding the customer, the checks before the card, and creating the
4
+ * document — so the three cannot drift apart.
5
+ *
6
+ * None of these findings can be fixed by the model: behind a chat order there
7
+ * is no document with a total to check against. They all go to the card.
8
+ */
9
+ import { type Contact, type LineItem, type SalesDocumentKind } from "@kevludwig/lexware-office";
10
+ import { z } from "zod";
11
+ import { type Warning } from "./findings";
12
+ export declare const DOCUMENT_LABEL: Record<SalesDocumentKind, string>;
13
+ /** Amounts must be cent-exact — more digits are a reading artefact. */
14
+ export declare function isCentExact(value: number): boolean;
15
+ /** Lexware accepts at most 300 line items per document. */
16
+ export declare const MAX_LINE_ITEMS = 300;
17
+ export declare const lineItemSchema: z.ZodObject<{
18
+ name: z.ZodString;
19
+ quantity: z.ZodNumber;
20
+ unit: z.ZodString;
21
+ net_price: z.ZodNumber;
22
+ tax_rate: z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<7>, z.ZodLiteral<19>]>;
23
+ }, z.core.$strict>;
24
+ export type LineItemInput = z.infer<typeof lineItemSchema>;
25
+ export declare const toLineItems: (items?: readonly Partial<LineItemInput>[]) => LineItem[];
26
+ export declare function isDocumentTypeLabel(title: string): boolean;
27
+ /** Whether yyyy-MM-dd names a real day — the pattern alone accepts 2026-02-30. */
28
+ export declare function isCalendarDate(value: string): boolean;
29
+ /** Title as the API allows it: max. 25 characters (a longer one answers 406), and no document type. */
30
+ export declare const titleSchema: (what: string) => z.ZodOptional<z.ZodString>;
31
+ export declare const customerFields: {
32
+ customer_number: z.ZodOptional<z.ZodNumber>;
33
+ contact_id: z.ZodOptional<z.ZodString>;
34
+ };
35
+ export interface SalesInput {
36
+ customer_number?: number;
37
+ contact_id?: string;
38
+ items?: readonly Partial<LineItemInput>[];
39
+ title?: string;
40
+ valid_until?: string;
41
+ source_document_id?: string;
42
+ source_document_type?: SalesDocumentKind;
43
+ }
44
+ /**
45
+ * The customer behind a number or contact id. There is no collective customer:
46
+ * a document to the wrong contact is wrong, not slightly off — an unresolvable
47
+ * one is a warning on the card and an error when writing.
48
+ */
49
+ export declare function resolveCustomer(input: SalesInput, signal?: AbortSignal): Promise<{
50
+ contact: Contact | null;
51
+ warnings: Warning[];
52
+ }>;
53
+ /** Customer, duplicates, and the source document — network errors become findings, not exceptions. */
54
+ export declare function runSalesPreflight(kind: SalesDocumentKind, input: SalesInput, signal?: AbortSignal): Promise<Warning[]>;
55
+ /** The approval policy of a sales tool: checks to state, then the card decides. */
56
+ export declare function salesApproval(kind: SalesDocumentKind, normalize?: (input: SalesInput) => SalesInput): (ctx: {
57
+ toolInput?: unknown;
58
+ callId: string;
59
+ abortSignal?: AbortSignal;
60
+ }) => Promise<"user-approval">;
61
+ /** Default validity of a quotation when the user names none. */
62
+ export declare const QUOTATION_VALIDITY_DAYS = 30;
63
+ /**
64
+ * Creates the document: quotations and order confirmations finalized (they
65
+ * get their number now), invoices as drafts — finalized and sent in Lexware
66
+ * Office. A replay of the same call does not create twice; a document that
67
+ * appeared between card and click stops the write.
68
+ */
69
+ export declare function executeSalesDocument(kind: SalesDocumentKind, input: SalesInput, ctx: {
70
+ callId: string;
71
+ abortSignal?: AbortSignal;
72
+ }): Promise<Record<string, unknown>>;