kassza 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.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/agents/README.md +59 -0
- package/agents/api.md +206 -0
- package/agents/pitfalls.md +52 -0
- package/agents/recipes.md +217 -0
- package/agents/skills/kassza/SKILL.md +29 -0
- package/assets/logo-wordmark.svg +25 -0
- package/assets/logo.svg +19 -0
- package/dist/binary-B89HM03_.cjs +63 -0
- package/dist/binary-D0M9U2MD.js +34 -0
- package/dist/client-B9kbKKBf.d.cts +748 -0
- package/dist/client-zv4enyq7.d.ts +748 -0
- package/dist/cookie-stores/index.cjs +153 -0
- package/dist/cookie-stores/index.d.cts +63 -0
- package/dist/cookie-stores/index.d.ts +63 -0
- package/dist/cookie-stores/index.js +144 -0
- package/dist/create-BZd6CUO7.cjs +1160 -0
- package/dist/create-DDZaNlOz.js +939 -0
- package/dist/dates-D2XebOIg.js +34 -0
- package/dist/dates-DtHzwNU6.d.cts +7 -0
- package/dist/dates-DtHzwNU6.d.ts +7 -0
- package/dist/dates-PPxH0n5H.cjs +57 -0
- package/dist/errors-B0QJhUW1.cjs +302 -0
- package/dist/errors-DapdV5DK.js +273 -0
- package/dist/index-CYAGCdKJ.d.cts +56 -0
- package/dist/index-CYAGCdKJ.d.ts +56 -0
- package/dist/index.cjs +1406 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1394 -0
- package/dist/ipn/index.cjs +108 -0
- package/dist/ipn/index.d.cts +32 -0
- package/dist/ipn/index.d.ts +32 -0
- package/dist/ipn/index.js +101 -0
- package/dist/money/index.cjs +15 -0
- package/dist/money/index.d.cts +2 -0
- package/dist/money/index.d.ts +2 -0
- package/dist/money/index.js +2 -0
- package/dist/money-C9j7nBNP.js +258 -0
- package/dist/money-DkiGukAw.cjs +335 -0
- package/dist/session-CRGOuz-R.cjs +100 -0
- package/dist/session-Dm_45x8K.d.cts +11 -0
- package/dist/session-Dm_45x8K.d.ts +11 -0
- package/dist/session-OJMLGufT.js +77 -0
- package/dist/shared-CrxlxI8n.cjs +207 -0
- package/dist/shared-D6DLa4b7.js +100 -0
- package/dist/storage/fs.cjs +88 -0
- package/dist/storage/fs.d.cts +13 -0
- package/dist/storage/fs.d.ts +13 -0
- package/dist/storage/fs.js +87 -0
- package/dist/storage/index.cjs +643 -0
- package/dist/storage/index.d.cts +288 -0
- package/dist/storage/index.d.ts +288 -0
- package/dist/storage/index.js +619 -0
- package/dist/testing/index.cjs +409 -0
- package/dist/testing/index.d.cts +35 -0
- package/dist/testing/index.d.ts +35 -0
- package/dist/testing/index.js +407 -0
- package/dist/types-DVRgwcqg.d.cts +26 -0
- package/dist/types-DVRgwcqg.d.ts +26 -0
- package/dist/validators/index.cjs +296 -0
- package/dist/validators/index.d.cts +51 -0
- package/dist/validators/index.d.ts +51 -0
- package/dist/validators/index.js +278 -0
- package/llms.txt +16 -0
- package/package.json +151 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
import { a as parseErrorCode, n as SzamlazzError, r as createAgentError } from "./errors-DapdV5DK.js";
|
|
2
|
+
import { a as isPdf, i as encodeUtf8, r as decodeUtf8, t as base64ToBytes } from "./binary-D0M9U2MD.js";
|
|
3
|
+
import { n as toAgentDate, t as addBudapestDays } from "./dates-D2XebOIg.js";
|
|
4
|
+
import { c as isHuf, f as decimalPlaces, p as roundMoney, r as calculateReceiptItem, t as calculateInvoiceItem, u as vatPercentage } from "./money-C9j7nBNP.js";
|
|
5
|
+
//#region src/core/actions.ts
|
|
6
|
+
const SZAMLAZZ_AGENT_URL = "https://www.szamlazz.hu/szamla/";
|
|
7
|
+
const AGENT_ACTIONS = {
|
|
8
|
+
createInvoice: "action-xmlagentxmlfile",
|
|
9
|
+
reverseInvoice: "action-szamla_agent_st",
|
|
10
|
+
registerPayment: "action-szamla_agent_kifiz",
|
|
11
|
+
getInvoicePdf: "action-szamla_agent_pdf",
|
|
12
|
+
getInvoiceXml: "action-szamla_agent_xml",
|
|
13
|
+
deleteProforma: "action-szamla_agent_dijbekero_torlese",
|
|
14
|
+
createReceipt: "action-szamla_agent_nyugta_create",
|
|
15
|
+
reverseReceipt: "action-szamla_agent_nyugta_storno",
|
|
16
|
+
getReceipt: "action-szamla_agent_nyugta_get",
|
|
17
|
+
sendReceipt: "action-szamla_agent_nyugta_send",
|
|
18
|
+
queryTaxpayer: "action-szamla_agent_taxpayer"
|
|
19
|
+
};
|
|
20
|
+
const MAX_INVOICE_ATTACHMENTS = 5;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/core/xml/parse.ts
|
|
23
|
+
var XmlParseError = class extends Error {
|
|
24
|
+
name = "XmlParseError";
|
|
25
|
+
};
|
|
26
|
+
const NAMED_ENTITIES = {
|
|
27
|
+
amp: "&",
|
|
28
|
+
lt: "<",
|
|
29
|
+
gt: ">",
|
|
30
|
+
quot: "\"",
|
|
31
|
+
apos: "'"
|
|
32
|
+
};
|
|
33
|
+
function decodeEntities(value) {
|
|
34
|
+
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, entity) => {
|
|
35
|
+
if (entity.startsWith("#x")) return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
36
|
+
if (entity.startsWith("#")) return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
37
|
+
return NAMED_ENTITIES[entity] ?? match;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function localName(qualifiedName) {
|
|
41
|
+
const colon = qualifiedName.indexOf(":");
|
|
42
|
+
return colon === -1 ? qualifiedName : qualifiedName.slice(colon + 1);
|
|
43
|
+
}
|
|
44
|
+
const ATTRIBUTE_PATTERN = /([^\s=/>]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
45
|
+
function parseAttributes(source) {
|
|
46
|
+
const attributes = {};
|
|
47
|
+
for (const match of source.matchAll(ATTRIBUTE_PATTERN)) {
|
|
48
|
+
const [, name, , doubleQuoted, singleQuoted] = match;
|
|
49
|
+
if (name === void 0) continue;
|
|
50
|
+
attributes[name] = decodeEntities(doubleQuoted ?? singleQuoted ?? "");
|
|
51
|
+
}
|
|
52
|
+
return attributes;
|
|
53
|
+
}
|
|
54
|
+
function skipUntil(xml, from, terminator) {
|
|
55
|
+
const end = xml.indexOf(terminator, from);
|
|
56
|
+
if (end === -1) throw new XmlParseError(`Lezáratlan szerkezet, hiányzik: ${terminator}`);
|
|
57
|
+
return end + terminator.length;
|
|
58
|
+
}
|
|
59
|
+
function parseXml(xml) {
|
|
60
|
+
const stack = [];
|
|
61
|
+
let root;
|
|
62
|
+
let position = xml.charCodeAt(0) === 65279 ? 1 : 0;
|
|
63
|
+
const appendText = (value) => {
|
|
64
|
+
const current = stack.at(-1);
|
|
65
|
+
if (current) current.text += value;
|
|
66
|
+
};
|
|
67
|
+
while (position < xml.length) {
|
|
68
|
+
const tagStart = xml.indexOf("<", position);
|
|
69
|
+
if (tagStart === -1) {
|
|
70
|
+
appendText(decodeEntities(xml.slice(position)));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
if (tagStart > position) appendText(decodeEntities(xml.slice(position, tagStart)));
|
|
74
|
+
if (xml.startsWith("<!--", tagStart)) {
|
|
75
|
+
position = skipUntil(xml, tagStart + 4, "-->");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (xml.startsWith("<![CDATA[", tagStart)) {
|
|
79
|
+
const end = xml.indexOf("]]>", tagStart + 9);
|
|
80
|
+
if (end === -1) throw new XmlParseError("Lezáratlan CDATA szakasz");
|
|
81
|
+
appendText(xml.slice(tagStart + 9, end));
|
|
82
|
+
position = end + 3;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (xml.startsWith("<?", tagStart)) {
|
|
86
|
+
position = skipUntil(xml, tagStart + 2, "?>");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (xml.startsWith("<!", tagStart)) {
|
|
90
|
+
position = skipUntil(xml, tagStart + 2, ">");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
94
|
+
if (tagEnd === -1) throw new XmlParseError("Lezáratlan XML tag");
|
|
95
|
+
const rawTag = xml.slice(tagStart + 1, tagEnd);
|
|
96
|
+
position = tagEnd + 1;
|
|
97
|
+
if (rawTag.startsWith("/")) {
|
|
98
|
+
const closingName = localName(rawTag.slice(1).trim());
|
|
99
|
+
const current = stack.pop();
|
|
100
|
+
if (!current || current.name !== closingName) throw new XmlParseError(`Váratlan záró tag: ${closingName}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const selfClosing = rawTag.endsWith("/");
|
|
104
|
+
const tagBody = selfClosing ? rawTag.slice(0, -1) : rawTag;
|
|
105
|
+
const nameMatch = /^\s*([^\s/>]+)/.exec(tagBody);
|
|
106
|
+
if (!nameMatch?.[1]) throw new XmlParseError("Érvénytelen XML tag");
|
|
107
|
+
const element = {
|
|
108
|
+
name: localName(nameMatch[1]),
|
|
109
|
+
attributes: parseAttributes(tagBody.slice(nameMatch[0].length)),
|
|
110
|
+
children: [],
|
|
111
|
+
text: ""
|
|
112
|
+
};
|
|
113
|
+
const parent = stack.at(-1);
|
|
114
|
+
if (parent) parent.children.push(element);
|
|
115
|
+
else if (root) throw new XmlParseError("Az XML-nek csak egy gyökéreleme lehet");
|
|
116
|
+
else root = element;
|
|
117
|
+
if (!selfClosing) stack.push(element);
|
|
118
|
+
}
|
|
119
|
+
if (stack.length > 0) throw new XmlParseError(`Lezáratlan elem: ${stack.at(-1)?.name}`);
|
|
120
|
+
if (!root) throw new XmlParseError("Üres XML dokumentum");
|
|
121
|
+
return root;
|
|
122
|
+
}
|
|
123
|
+
function findChild(element, name) {
|
|
124
|
+
return element?.children.find((child) => child.name === name);
|
|
125
|
+
}
|
|
126
|
+
function findChildren(element, name) {
|
|
127
|
+
return element?.children.filter((child) => child.name === name) ?? [];
|
|
128
|
+
}
|
|
129
|
+
function childText(element, name) {
|
|
130
|
+
const child = findChild(element, name);
|
|
131
|
+
if (!child) return void 0;
|
|
132
|
+
const value = child.text.trim();
|
|
133
|
+
return value === "" ? void 0 : value;
|
|
134
|
+
}
|
|
135
|
+
function childNumber(element, name) {
|
|
136
|
+
const value = childText(element, name);
|
|
137
|
+
if (value === void 0) return void 0;
|
|
138
|
+
const parsed = Number(value.replace(",", "."));
|
|
139
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
140
|
+
}
|
|
141
|
+
function childBoolean(element, name) {
|
|
142
|
+
const value = childText(element, name)?.toLowerCase();
|
|
143
|
+
if (value === "true" || value === "1") return true;
|
|
144
|
+
if (value === "false" || value === "0") return false;
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/core/response.ts
|
|
148
|
+
function createAgentResponse(action, status, headers, body) {
|
|
149
|
+
let cachedText;
|
|
150
|
+
return {
|
|
151
|
+
action,
|
|
152
|
+
status,
|
|
153
|
+
headers,
|
|
154
|
+
body,
|
|
155
|
+
isPdf: isPdf(body),
|
|
156
|
+
text() {
|
|
157
|
+
cachedText ??= decodeUtf8(body);
|
|
158
|
+
return cachedText;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function decodeHeaderValue(value) {
|
|
163
|
+
try {
|
|
164
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
165
|
+
} catch {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function readHeader(headers, name, encoded = false) {
|
|
170
|
+
const raw = headers.get(name);
|
|
171
|
+
if (raw === null) return void 0;
|
|
172
|
+
const value = (encoded ? decodeHeaderValue(raw) : raw).trim();
|
|
173
|
+
return value === "" ? void 0 : value;
|
|
174
|
+
}
|
|
175
|
+
function readNumberHeader(headers, name) {
|
|
176
|
+
const value = readHeader(headers, name);
|
|
177
|
+
if (value === void 0) return void 0;
|
|
178
|
+
const parsed = Number(value.replace(",", "."));
|
|
179
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
180
|
+
}
|
|
181
|
+
function readDocumentHeaders(headers) {
|
|
182
|
+
return {
|
|
183
|
+
number: readHeader(headers, "szlahu_szamlaszam", true),
|
|
184
|
+
netTotal: readNumberHeader(headers, "szlahu_nettovegosszeg"),
|
|
185
|
+
grossTotal: readNumberHeader(headers, "szlahu_bruttovegosszeg"),
|
|
186
|
+
paymentMethod: readHeader(headers, "szlahu_fizetesmod", true),
|
|
187
|
+
buyerAccountUrl: readHeader(headers, "szlahu_vevoifiokurl", true)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const RAW_RESPONSE_PREVIEW_LENGTH = 2e3;
|
|
191
|
+
function rawPreview(response) {
|
|
192
|
+
if (response.isPdf) return void 0;
|
|
193
|
+
return response.text().slice(0, RAW_RESPONSE_PREVIEW_LENGTH);
|
|
194
|
+
}
|
|
195
|
+
function throwIfHeaderError(response) {
|
|
196
|
+
const message = readHeader(response.headers, "szlahu_error", true);
|
|
197
|
+
const code = parseErrorCode(response.headers.get("szlahu_error_code"));
|
|
198
|
+
if (message === void 0 && code === void 0) return;
|
|
199
|
+
throw createAgentError({
|
|
200
|
+
code,
|
|
201
|
+
message,
|
|
202
|
+
action: response.action,
|
|
203
|
+
httpStatus: response.status,
|
|
204
|
+
rawResponse: rawPreview(response)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function extractTextError(text) {
|
|
208
|
+
const start = text.indexOf("[ERR]");
|
|
209
|
+
if (start === -1) return void 0;
|
|
210
|
+
const afterMarker = text.slice(start + 5);
|
|
211
|
+
const separator = afterMarker.search(/-{5,}/);
|
|
212
|
+
const message = (separator === -1 ? afterMarker.split("\n")[0] ?? "" : afterMarker.slice(0, separator)).replace(/\s+/g, " ").trim();
|
|
213
|
+
const codeMatch = /^\[?(\d{1,4})\]?\s+/.exec(message);
|
|
214
|
+
if (codeMatch?.[1]) return {
|
|
215
|
+
code: Number(codeMatch[1]),
|
|
216
|
+
message: message.slice(codeMatch[0].length)
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
code: void 0,
|
|
220
|
+
message
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function throwIfTextError(response) {
|
|
224
|
+
if (response.isPdf) return;
|
|
225
|
+
const text = response.text().trimStart();
|
|
226
|
+
if (!text.startsWith("[ERR]")) return;
|
|
227
|
+
const error = extractTextError(text);
|
|
228
|
+
if (!error) return;
|
|
229
|
+
throw createAgentError({
|
|
230
|
+
code: error.code,
|
|
231
|
+
message: error.message,
|
|
232
|
+
action: response.action,
|
|
233
|
+
httpStatus: response.status,
|
|
234
|
+
rawResponse: rawPreview(response)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function parseDoneText(text) {
|
|
238
|
+
const match = /xmlagentresponse\s*=\s*DONE(?:;([^\r\n]*))?/i.exec(text);
|
|
239
|
+
if (!match) return void 0;
|
|
240
|
+
const number = match[1]?.trim();
|
|
241
|
+
return { number: number ? decodeHeaderValue(number) : void 0 };
|
|
242
|
+
}
|
|
243
|
+
function unexpectedResponse(response, detail) {
|
|
244
|
+
const suffix = detail ? ` ${detail}` : "";
|
|
245
|
+
return new SzamlazzError(`Váratlan válasz a Számlázz.hu-tól (HTTP ${response.status}).${suffix}`, {
|
|
246
|
+
category: "unexpected_response",
|
|
247
|
+
action: response.action,
|
|
248
|
+
httpStatus: response.status,
|
|
249
|
+
rawResponse: rawPreview(response)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function looksLikeXml(response) {
|
|
253
|
+
if (response.isPdf) return false;
|
|
254
|
+
const text = response.text();
|
|
255
|
+
return /^\s*</.test(text.charCodeAt(0) === 65279 ? text.slice(1) : text);
|
|
256
|
+
}
|
|
257
|
+
function parseResponseXml(response) {
|
|
258
|
+
if (!looksLikeXml(response)) throw unexpectedResponse(response, "XML választ vártunk.");
|
|
259
|
+
try {
|
|
260
|
+
return parseXml(response.text());
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new SzamlazzError(`A Számlázz.hu XML válasza nem értelmezhető (HTTP ${response.status}).`, {
|
|
263
|
+
category: "unexpected_response",
|
|
264
|
+
action: response.action,
|
|
265
|
+
httpStatus: response.status,
|
|
266
|
+
rawResponse: rawPreview(response),
|
|
267
|
+
cause: error
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function throwIfXmlFailure(root, response) {
|
|
272
|
+
const success = childText(root, "sikeres")?.toLowerCase();
|
|
273
|
+
const code = childNumber(root, "hibakod");
|
|
274
|
+
const message = childText(root, "hibauzenet");
|
|
275
|
+
if (success === "true" && code === void 0) return;
|
|
276
|
+
if (success === void 0 && code === void 0 && message === void 0) return;
|
|
277
|
+
throw createAgentError({
|
|
278
|
+
code,
|
|
279
|
+
message,
|
|
280
|
+
action: response.action,
|
|
281
|
+
httpStatus: response.status,
|
|
282
|
+
rawResponse: rawPreview(response)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function throwIfHttpError(response) {
|
|
286
|
+
if (response.status >= 200 && response.status < 400) return;
|
|
287
|
+
const error = unexpectedResponse(response);
|
|
288
|
+
if (response.status >= 500) throw new SzamlazzError(error.message, {
|
|
289
|
+
category: "network",
|
|
290
|
+
action: response.action,
|
|
291
|
+
httpStatus: response.status,
|
|
292
|
+
rawResponse: error.rawResponse
|
|
293
|
+
});
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
function throwIfAnyError(response) {
|
|
297
|
+
throwIfHeaderError(response);
|
|
298
|
+
throwIfTextError(response);
|
|
299
|
+
throwIfHttpError(response);
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/core/xml/serialize.ts
|
|
303
|
+
function el(name, content) {
|
|
304
|
+
return {
|
|
305
|
+
name,
|
|
306
|
+
content
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function optionalEl(name, content) {
|
|
310
|
+
if (content === void 0 || content === null) return void 0;
|
|
311
|
+
if (Array.isArray(content) && !content.some(Boolean)) return void 0;
|
|
312
|
+
return {
|
|
313
|
+
name,
|
|
314
|
+
content
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function isAllowedXmlChar(codePoint) {
|
|
318
|
+
if (codePoint === 9 || codePoint === 10 || codePoint === 13) return true;
|
|
319
|
+
if (codePoint < 32) return false;
|
|
320
|
+
return codePoint !== 65534 && codePoint !== 65535;
|
|
321
|
+
}
|
|
322
|
+
function stripInvalidXmlChars(value) {
|
|
323
|
+
let result = "";
|
|
324
|
+
for (const char of value) if (isAllowedXmlChar(char.codePointAt(0) ?? 0)) result += char;
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
function escapeXml(value) {
|
|
328
|
+
return stripInvalidXmlChars(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
329
|
+
}
|
|
330
|
+
function formatXmlNumber(value) {
|
|
331
|
+
if (!Number.isFinite(value)) throw new RangeError(`Az XML-be csak véges szám írható, kapott: ${value}`);
|
|
332
|
+
if (Number.isInteger(value)) return value.toString();
|
|
333
|
+
const fixed = value.toFixed(10).replace(/0+$/, "").replace(/\.$/, "");
|
|
334
|
+
return fixed === "-0" ? "0" : fixed;
|
|
335
|
+
}
|
|
336
|
+
function formatScalar(value) {
|
|
337
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
338
|
+
if (typeof value === "number") return formatXmlNumber(value);
|
|
339
|
+
return escapeXml(value);
|
|
340
|
+
}
|
|
341
|
+
function isPresent(child) {
|
|
342
|
+
return Boolean(child);
|
|
343
|
+
}
|
|
344
|
+
function renderNode(node, depth) {
|
|
345
|
+
const indent = " ".repeat(depth);
|
|
346
|
+
const { name, content } = node;
|
|
347
|
+
if (content === void 0 || content === null) return `${indent}<${name}></${name}>`;
|
|
348
|
+
if (!Array.isArray(content)) return `${indent}<${name}>${formatScalar(content)}</${name}>`;
|
|
349
|
+
const children = content.filter(isPresent);
|
|
350
|
+
if (children.length === 0) return `${indent}<${name}></${name}>`;
|
|
351
|
+
return `${indent}<${name}>\n${children.map((child) => renderNode(child, depth + 1)).join("\n")}\n${indent}</${name}>`;
|
|
352
|
+
}
|
|
353
|
+
function buildXmlDocument(options) {
|
|
354
|
+
const attributes = [`xmlns="${options.namespace}"`];
|
|
355
|
+
if (options.schemaLocation !== void 0) attributes.push("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", `xsi:schemaLocation="${options.namespace} ${options.schemaLocation}"`);
|
|
356
|
+
const body = options.children.filter(isPresent).map((child) => renderNode(child, 1)).join("\n");
|
|
357
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<${options.root} ${attributes.join(" ")}>\n${body}\n</${options.root}>\n`;
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/invoices/create-types.ts
|
|
361
|
+
const INVOICE_TYPES = [
|
|
362
|
+
"invoice",
|
|
363
|
+
"proforma",
|
|
364
|
+
"advance",
|
|
365
|
+
"final",
|
|
366
|
+
"corrective",
|
|
367
|
+
"deliveryNote"
|
|
368
|
+
];
|
|
369
|
+
const INVOICE_LANGUAGES = [
|
|
370
|
+
"hu",
|
|
371
|
+
"en",
|
|
372
|
+
"de",
|
|
373
|
+
"it",
|
|
374
|
+
"ro",
|
|
375
|
+
"sk",
|
|
376
|
+
"hr",
|
|
377
|
+
"fr",
|
|
378
|
+
"es",
|
|
379
|
+
"cz",
|
|
380
|
+
"pl",
|
|
381
|
+
"bg",
|
|
382
|
+
"nl",
|
|
383
|
+
"ru",
|
|
384
|
+
"si"
|
|
385
|
+
];
|
|
386
|
+
const INVOICE_TEMPLATES = [
|
|
387
|
+
"SzlaMost",
|
|
388
|
+
"SzlaAlap",
|
|
389
|
+
"SzlaNoEnv",
|
|
390
|
+
"Szla8cm",
|
|
391
|
+
"SzlaTomb",
|
|
392
|
+
"SzlaFuvarlevelesAlap"
|
|
393
|
+
];
|
|
394
|
+
const TAXPAYER_TYPE_CODES = {
|
|
395
|
+
nonEuBusiness: 7,
|
|
396
|
+
euBusiness: 6,
|
|
397
|
+
hungarianTaxNumber: 1,
|
|
398
|
+
unknown: 0,
|
|
399
|
+
noTaxNumber: -1
|
|
400
|
+
};
|
|
401
|
+
const SIMPLE_ITEMS_MAX = 2;
|
|
402
|
+
const SIMPLE_ITEMS_FINAL_MAX = 4;
|
|
403
|
+
const SIMPLE_ITEMS_VAT_RATES = /* @__PURE__ */ new Set([
|
|
404
|
+
0,
|
|
405
|
+
5,
|
|
406
|
+
18,
|
|
407
|
+
27,
|
|
408
|
+
"TAM",
|
|
409
|
+
"AAM",
|
|
410
|
+
"K.AFA",
|
|
411
|
+
"F.AFA"
|
|
412
|
+
]);
|
|
413
|
+
const LANGUAGES = new Set(INVOICE_LANGUAGES);
|
|
414
|
+
const TEMPLATES = new Set(INVOICE_TEMPLATES);
|
|
415
|
+
const TYPES = new Set(INVOICE_TYPES);
|
|
416
|
+
function invalid(message, cause) {
|
|
417
|
+
return new SzamlazzError(message, {
|
|
418
|
+
category: "validation",
|
|
419
|
+
cause
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function presentText(value) {
|
|
423
|
+
return value === void 0 || value.trim() === "" ? void 0 : value;
|
|
424
|
+
}
|
|
425
|
+
function agentDate(value, field) {
|
|
426
|
+
try {
|
|
427
|
+
return toAgentDate(value);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
throw invalid(`A(z) ${field} mező nem érvényes dátum: ${String(value)}`, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function optionalAgentDate(value, field) {
|
|
433
|
+
return value === void 0 ? void 0 : agentDate(value, field);
|
|
434
|
+
}
|
|
435
|
+
function requireText$1(value, message) {
|
|
436
|
+
if (typeof value !== "string" || value.trim() === "") throw invalid(message);
|
|
437
|
+
}
|
|
438
|
+
function assertNonNegativeInteger(value, field) {
|
|
439
|
+
if (value === void 0) return;
|
|
440
|
+
if (!Number.isInteger(value) || value < 0) throw invalid(`A(z) ${field} mező nemnegatív egész szám legyen, kapott: ${value}`);
|
|
441
|
+
}
|
|
442
|
+
function assertFinite(value, field) {
|
|
443
|
+
if (value !== void 0 && !Number.isFinite(value)) throw invalid(`A(z) ${field} mező nem érvényes szám: ${value}`);
|
|
444
|
+
}
|
|
445
|
+
function mergeSeller(defaults = {}, input = {}) {
|
|
446
|
+
return {
|
|
447
|
+
bank: input.bank ?? defaults.bank,
|
|
448
|
+
bankAccount: input.bankAccount ?? defaults.bankAccount,
|
|
449
|
+
emailReplyTo: input.emailReplyTo ?? defaults.emailReplyTo,
|
|
450
|
+
emailSubject: input.emailSubject ?? defaults.emailSubject,
|
|
451
|
+
emailText: input.emailText ?? defaults.emailText,
|
|
452
|
+
signatoryName: input.signatoryName ?? defaults.signatoryName
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function resolveType(input) {
|
|
456
|
+
const type = input.type ?? "invoice";
|
|
457
|
+
if (!TYPES.has(type)) throw invalid(`Ismeretlen bizonylattípus: ${String(type)}`);
|
|
458
|
+
if (input.type === "corrective") requireText$1(input.correctedInvoiceNumber, "Helyesbítő számlához add meg a helyesbített számla számát (correctedInvoiceNumber).");
|
|
459
|
+
if (input.type === "final" && !presentText(input.advanceInvoiceNumber)) requireText$1(input.orderNumber, "Végszámlához add meg az előlegszámla számát (advanceInvoiceNumber) vagy az előlegszámla rendelésszámát (orderNumber).");
|
|
460
|
+
return type;
|
|
461
|
+
}
|
|
462
|
+
function resolveDates(defaults, input, now) {
|
|
463
|
+
if (input.dueDate !== void 0 && input.paymentDueInDays !== void 0) throw invalid("A dueDate és a paymentDueInDays közül csak az egyiket add meg.");
|
|
464
|
+
const issueDate = optionalAgentDate(input.issueDate, "issueDate") ?? agentDate(now, "now");
|
|
465
|
+
const fulfillmentDate = optionalAgentDate(input.fulfillmentDate, "fulfillmentDate") ?? issueDate;
|
|
466
|
+
const explicitDue = optionalAgentDate(input.dueDate, "dueDate");
|
|
467
|
+
if (explicitDue !== void 0) return {
|
|
468
|
+
issueDate,
|
|
469
|
+
fulfillmentDate,
|
|
470
|
+
dueDate: explicitDue
|
|
471
|
+
};
|
|
472
|
+
const days = input.paymentDueInDays ?? defaults.paymentDueInDays ?? 0;
|
|
473
|
+
assertNonNegativeInteger(days, "paymentDueInDays");
|
|
474
|
+
return {
|
|
475
|
+
issueDate,
|
|
476
|
+
fulfillmentDate,
|
|
477
|
+
dueDate: addBudapestDays(/* @__PURE__ */ new Date(`${issueDate}T12:00:00Z`), days)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function resolveCurrency(defaults, input) {
|
|
481
|
+
const currency = presentText(input.currency) ?? presentText(defaults.currency) ?? "HUF";
|
|
482
|
+
const exchangeRate = input.exchangeRate;
|
|
483
|
+
if (isHuf(currency)) {
|
|
484
|
+
if (exchangeRate !== void 0 || presentText(input.exchangeBank) !== void 0) throw invalid("Forintos számlán nem adható meg árfolyam (exchangeRate, exchangeBank).");
|
|
485
|
+
return {
|
|
486
|
+
currency,
|
|
487
|
+
exchangeBank: void 0,
|
|
488
|
+
exchangeRate: void 0
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (exchangeRate !== void 0 && !(Number.isFinite(exchangeRate) && exchangeRate > 0)) throw invalid(`Az árfolyam (exchangeRate) pozitív szám legyen, kapott: ${exchangeRate}`);
|
|
492
|
+
const exchangeBank = presentText(input.exchangeBank) ?? presentText(defaults.exchangeBank) ?? "MNB";
|
|
493
|
+
if (exchangeRate === void 0 && exchangeBank.trim().toUpperCase() !== "MNB") throw invalid(`Devizás számlához (${currency}) add meg az árfolyamot (exchangeRate). Árfolyam nélkül csak az MNB automatikus árfolyama használható.`);
|
|
494
|
+
return {
|
|
495
|
+
currency,
|
|
496
|
+
exchangeBank,
|
|
497
|
+
exchangeRate
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function resolveItems(items, currency) {
|
|
501
|
+
if (!Array.isArray(items) || items.length === 0) throw invalid("A számlán legalább egy tételnek szerepelnie kell.");
|
|
502
|
+
return items.map((item, index) => {
|
|
503
|
+
const label = `${index + 1}. tétel`;
|
|
504
|
+
requireText$1(item.name, `A(z) ${label} megnevezése (name) nem lehet üres.`);
|
|
505
|
+
const unit = item.unit ?? "db";
|
|
506
|
+
requireText$1(unit, `A(z) ${label} mennyiségi egysége (unit) nem lehet üres.`);
|
|
507
|
+
assertNonNegativeInteger(item.dataDeletionCode, `${label} dataDeletionCode`);
|
|
508
|
+
assertFinite(item.marginVatBase, `${label} marginVatBase`);
|
|
509
|
+
let amounts;
|
|
510
|
+
try {
|
|
511
|
+
amounts = calculateInvoiceItem(item, currency);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
throw invalid(`${label} (${item.name}): ${message}`, error);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
input: item,
|
|
518
|
+
unit,
|
|
519
|
+
amounts: {
|
|
520
|
+
name: item.name,
|
|
521
|
+
...amounts
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function validateSimpleItems(type, items) {
|
|
527
|
+
if (type === "corrective" || type === "deliveryNote") throw invalid("Egyszerűsített számlakép (simpleItems) helyesbítő számlán és szállítólevélen nem használható.");
|
|
528
|
+
const max = type === "final" ? SIMPLE_ITEMS_FINAL_MAX : SIMPLE_ITEMS_MAX;
|
|
529
|
+
if (items.length > max) throw invalid(`Egyszerűsített számlaképen (simpleItems) legfeljebb ${max} tétel adható meg.`);
|
|
530
|
+
const invalidItem = items.find((item) => !SIMPLE_ITEMS_VAT_RATES.has(item.amounts.vat));
|
|
531
|
+
if (invalidItem) throw invalid(`Egyszerűsített számlaképen (simpleItems) csak 0, 5, 18, 27, TAM, AAM, K.AFA vagy F.AFA áfakulcs használható, kapott: ${String(invalidItem.amounts.vat)}`);
|
|
532
|
+
}
|
|
533
|
+
function attachmentSize(content) {
|
|
534
|
+
if (typeof content === "string") return encodeUtf8(content).byteLength;
|
|
535
|
+
if (content instanceof Blob) return content.size;
|
|
536
|
+
return content.byteLength;
|
|
537
|
+
}
|
|
538
|
+
function validateAttachments(attachments, sendEmail) {
|
|
539
|
+
if (attachments.length === 0) return attachments;
|
|
540
|
+
if (attachments.length > 5) throw invalid(`Legfeljebb 5 melléklet csatolható, kapott: ${attachments.length}`);
|
|
541
|
+
if (!sendEmail) throw invalid("Mellékletet csak e-mail értesítővel lehet küldeni: add meg a vevő e-mail címét, és ne kapcsold ki a sendEmail beállítást.");
|
|
542
|
+
for (const attachment of attachments) {
|
|
543
|
+
requireText$1(attachment.filename, "A melléklet fájlneve (filename) nem lehet üres.");
|
|
544
|
+
if (attachmentSize(attachment.content) > 2097152) throw invalid(`A(z) ${attachment.filename} melléklet nagyobb 2 MB-nál.`);
|
|
545
|
+
}
|
|
546
|
+
return attachments;
|
|
547
|
+
}
|
|
548
|
+
function validateWaybill(waybill) {
|
|
549
|
+
if (!waybill) return;
|
|
550
|
+
assertNonNegativeInteger(waybill.transOFlex?.packageCount, "waybill.transOFlex.packageCount");
|
|
551
|
+
assertNonNegativeInteger(waybill.sprinter?.packageCount, "waybill.sprinter.packageCount");
|
|
552
|
+
const mpl = waybill.mpl;
|
|
553
|
+
if (!mpl) return;
|
|
554
|
+
requireText$1(mpl.customerCode, "Az MPL fuvarlevélhez add meg a vevőkódot (customerCode).");
|
|
555
|
+
requireText$1(mpl.barcode, "Az MPL fuvarlevélhez add meg a vonalkódot (barcode).");
|
|
556
|
+
requireText$1(String(mpl.weight ?? ""), "Az MPL fuvarlevélhez add meg a tömeget (weight).");
|
|
557
|
+
assertFinite(mpl.declaredValue, "waybill.mpl.declaredValue");
|
|
558
|
+
}
|
|
559
|
+
function resolveSendEmail(defaults, input) {
|
|
560
|
+
const hasEmail = presentText(input.buyer.email) !== void 0;
|
|
561
|
+
if (input.buyer.sendEmail === true && !hasEmail) throw invalid("A sendEmail be van kapcsolva, de a vevőnek nincs e-mail címe (buyer.email).");
|
|
562
|
+
if (!hasEmail) return false;
|
|
563
|
+
return input.buyer.sendEmail ?? defaults.sendEmail ?? true;
|
|
564
|
+
}
|
|
565
|
+
function validateBuyer(input) {
|
|
566
|
+
const buyer = input.buyer;
|
|
567
|
+
if (!buyer || typeof buyer !== "object") throw invalid("A vevő adatai (buyer) kötelezők.");
|
|
568
|
+
requireText$1(buyer.name, "A vevő neve (buyer.name) nem lehet üres.");
|
|
569
|
+
requireText$1(buyer.zip, "A vevő irányítószáma (buyer.zip) nem lehet üres.");
|
|
570
|
+
requireText$1(buyer.city, "A vevő települése (buyer.city) nem lehet üres.");
|
|
571
|
+
requireText$1(buyer.address, "A vevő címe (buyer.address) nem lehet üres.");
|
|
572
|
+
}
|
|
573
|
+
function resolveInvoice(defaults, input, now) {
|
|
574
|
+
validateBuyer(input);
|
|
575
|
+
const type = resolveType(input);
|
|
576
|
+
const language = input.language ?? defaults.language ?? "hu";
|
|
577
|
+
if (!LANGUAGES.has(language)) throw invalid(`Ismeretlen számlanyelv: ${String(language)}`);
|
|
578
|
+
const template = input.template ?? defaults.template;
|
|
579
|
+
if (template !== void 0 && !TEMPLATES.has(template)) throw invalid(`Ismeretlen számlasablon: ${String(template)}`);
|
|
580
|
+
assertFinite(input.paymentCorrection, "paymentCorrection");
|
|
581
|
+
const currency = resolveCurrency(defaults, input);
|
|
582
|
+
const dates = resolveDates(defaults, input, now);
|
|
583
|
+
const items = resolveItems(input.items, currency.currency);
|
|
584
|
+
const simpleItems = input.simpleItems ?? defaults.simpleItems;
|
|
585
|
+
if (simpleItems) validateSimpleItems(type, items);
|
|
586
|
+
validateWaybill(input.waybill);
|
|
587
|
+
const sendEmail = resolveSendEmail(defaults, input);
|
|
588
|
+
const attachments = validateAttachments(input.attachments ?? [], sendEmail);
|
|
589
|
+
return {
|
|
590
|
+
input,
|
|
591
|
+
type,
|
|
592
|
+
...dates,
|
|
593
|
+
...currency,
|
|
594
|
+
paymentMethod: presentText(input.paymentMethod) ?? presentText(defaults.paymentMethod) ?? "Átutalás",
|
|
595
|
+
language,
|
|
596
|
+
prefix: presentText(input.prefix) ?? presentText(defaults.prefix),
|
|
597
|
+
eInvoice: input.eInvoice ?? defaults.eInvoice ?? false,
|
|
598
|
+
downloadPdf: input.downloadPdf ?? defaults.downloadPdf ?? true,
|
|
599
|
+
template,
|
|
600
|
+
simpleItems,
|
|
601
|
+
euVat: input.euVat ?? defaults.euVat,
|
|
602
|
+
logoExtra: presentText(input.logoExtra) ?? presentText(defaults.logoExtra),
|
|
603
|
+
aggregator: presentText(input.aggregator) ?? presentText(defaults.aggregator),
|
|
604
|
+
guardian: input.guardian ?? defaults.guardian,
|
|
605
|
+
articleIdentifierInvoice: input.articleIdentifierInvoice ?? defaults.articleIdentifierInvoice,
|
|
606
|
+
seller: mergeSeller(defaults.seller, input.seller),
|
|
607
|
+
sendEmail,
|
|
608
|
+
items,
|
|
609
|
+
attachments
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/receipts/types.ts
|
|
614
|
+
const RECEIPT_ONLY_VAT_CODES = [
|
|
615
|
+
"ÁKK",
|
|
616
|
+
"MAA",
|
|
617
|
+
"EU",
|
|
618
|
+
"EUK"
|
|
619
|
+
];
|
|
620
|
+
const RECEIPT_PDF_TEMPLATES = [
|
|
621
|
+
"A",
|
|
622
|
+
"N",
|
|
623
|
+
"J",
|
|
624
|
+
"L"
|
|
625
|
+
];
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/receipts/common.ts
|
|
628
|
+
const RECEIPT_XSD_BASE_URL = "https://www.szamlazz.hu/szamla/docs/xsds/";
|
|
629
|
+
function receiptValidationError(message) {
|
|
630
|
+
return new SzamlazzError(message, { category: "validation" });
|
|
631
|
+
}
|
|
632
|
+
function requireText(value, message) {
|
|
633
|
+
const trimmed = value?.trim();
|
|
634
|
+
if (!trimmed) throw receiptValidationError(message);
|
|
635
|
+
return trimmed;
|
|
636
|
+
}
|
|
637
|
+
function optionalText(value) {
|
|
638
|
+
const trimmed = value?.trim();
|
|
639
|
+
return trimmed ? trimmed : void 0;
|
|
640
|
+
}
|
|
641
|
+
const templates = new Set(RECEIPT_PDF_TEMPLATES);
|
|
642
|
+
function validateTemplate(template) {
|
|
643
|
+
if (template === void 0) return void 0;
|
|
644
|
+
if (!templates.has(template)) throw receiptValidationError(`Ismeretlen nyugta PDF sablon: ${String(template)}. Lehetséges értékek: A (normál A4), N (80 mm), J (jegy), L (jegy logóval).`);
|
|
645
|
+
return template;
|
|
646
|
+
}
|
|
647
|
+
function requireReceiptNumber(value) {
|
|
648
|
+
return requireText(value, "Add meg a nyugtaszámot (receiptNumber).");
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/receipts/parse.ts
|
|
652
|
+
function requiredText(element, name, response) {
|
|
653
|
+
const value = childText(element, name);
|
|
654
|
+
if (value === void 0) throw unexpectedResponse(response, `Hiányzik a(z) <${name}> elem a nyugta válaszból.`);
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
function amount(element, names) {
|
|
658
|
+
for (const name of names) {
|
|
659
|
+
const value = childNumber(element, name);
|
|
660
|
+
if (value !== void 0) return value;
|
|
661
|
+
}
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
function vatOf(element) {
|
|
665
|
+
const vatPercentage = childNumber(element, "afakulcs") ?? 0;
|
|
666
|
+
return {
|
|
667
|
+
vat: childText(element, "afatipus") ?? vatPercentage,
|
|
668
|
+
vatPercentage
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function parseLedger(element) {
|
|
672
|
+
const ledger = findChild(element, "fokonyv");
|
|
673
|
+
const revenue = childText(ledger, "arbevetel");
|
|
674
|
+
const vat = childText(ledger, "afa");
|
|
675
|
+
if (revenue === void 0 && vat === void 0) return void 0;
|
|
676
|
+
return {
|
|
677
|
+
revenue,
|
|
678
|
+
vat
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function parseItem(element) {
|
|
682
|
+
return {
|
|
683
|
+
name: childText(element, "megnevezes") ?? "",
|
|
684
|
+
identifier: childText(element, "azonosito"),
|
|
685
|
+
quantity: childNumber(element, "mennyiseg") ?? 0,
|
|
686
|
+
unit: childText(element, "mennyisegiEgyseg") ?? "",
|
|
687
|
+
netUnitPrice: childNumber(element, "nettoEgysegar") ?? 0,
|
|
688
|
+
...vatOf(element),
|
|
689
|
+
netAmount: amount(element, ["netto", "nettoErtek"]),
|
|
690
|
+
vatAmount: amount(element, ["afa", "afaErtek"]),
|
|
691
|
+
grossAmount: amount(element, ["brutto", "bruttoErtek"]),
|
|
692
|
+
ledger: parseLedger(element)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function parsePayment(element) {
|
|
696
|
+
return {
|
|
697
|
+
method: childText(element, "fizetoeszkoz") ?? "",
|
|
698
|
+
amount: childNumber(element, "osszeg") ?? 0,
|
|
699
|
+
description: childText(element, "leiras")
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function parseVatTotal(element) {
|
|
703
|
+
return {
|
|
704
|
+
...vatOf(element),
|
|
705
|
+
netAmount: amount(element, ["netto"]),
|
|
706
|
+
vatAmount: amount(element, ["afa"]),
|
|
707
|
+
grossAmount: amount(element, ["brutto"])
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function parseTotals(receipt, items) {
|
|
711
|
+
const totals = findChild(receipt, "osszegek");
|
|
712
|
+
const byVat = findChildren(totals, "afakulcsossz").map(parseVatTotal);
|
|
713
|
+
const total = findChild(totals, "totalossz");
|
|
714
|
+
if (total) return {
|
|
715
|
+
netAmount: amount(total, ["netto"]),
|
|
716
|
+
vatAmount: amount(total, ["afa"]),
|
|
717
|
+
grossAmount: amount(total, ["brutto"]),
|
|
718
|
+
byVat
|
|
719
|
+
};
|
|
720
|
+
const sum = (pick) => Number(items.reduce((acc, item) => acc + pick(item), 0).toFixed(2));
|
|
721
|
+
return {
|
|
722
|
+
netAmount: sum((item) => item.netAmount),
|
|
723
|
+
vatAmount: sum((item) => item.vatAmount),
|
|
724
|
+
grossAmount: sum((item) => item.grossAmount),
|
|
725
|
+
byVat
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function parsePdf(root) {
|
|
729
|
+
const encoded = childText(root, "nyugtaPdf");
|
|
730
|
+
if (encoded === void 0) return void 0;
|
|
731
|
+
const bytes = base64ToBytes(encoded);
|
|
732
|
+
return bytes.length > 0 ? bytes : void 0;
|
|
733
|
+
}
|
|
734
|
+
function parseReceiptXml(root, response) {
|
|
735
|
+
const receipt = findChild(root, "nyugta");
|
|
736
|
+
const base = findChild(receipt, "alap");
|
|
737
|
+
if (!receipt || !base) throw unexpectedResponse(response, "A nyugta válaszból hiányzik a <nyugta> blokk.");
|
|
738
|
+
const items = findChildren(findChild(receipt, "tetelek"), "tetel").map(parseItem);
|
|
739
|
+
const pdf = parsePdf(root);
|
|
740
|
+
return {
|
|
741
|
+
id: childNumber(base, "id") ?? 0,
|
|
742
|
+
number: requiredText(base, "nyugtaszam", response),
|
|
743
|
+
callId: childText(base, "hivasAzonosito"),
|
|
744
|
+
type: childText(base, "tipus")?.toUpperCase() === "SN" ? "reversal" : "receipt",
|
|
745
|
+
isReversed: childBoolean(base, "stornozott") ?? false,
|
|
746
|
+
reversedReceiptNumber: childText(base, "stornozottNyugtaszam"),
|
|
747
|
+
issueDate: childText(base, "kelt") ?? "",
|
|
748
|
+
paymentMethod: childText(base, "fizmod") ?? "",
|
|
749
|
+
currency: childText(base, "penznem") ?? "HUF",
|
|
750
|
+
exchangeBank: childText(base, "devizabank"),
|
|
751
|
+
exchangeRate: childNumber(base, "devizaarf"),
|
|
752
|
+
comment: childText(base, "megjegyzes"),
|
|
753
|
+
customerLedgerId: childText(base, "fokonyvVevo"),
|
|
754
|
+
isTest: childBoolean(base, "teszt") ?? false,
|
|
755
|
+
orderNumber: childText(base, "rendelesSzam"),
|
|
756
|
+
items,
|
|
757
|
+
payments: findChildren(findChild(receipt, "kifizetesek"), "kifizetes").map(parsePayment),
|
|
758
|
+
totals: parseTotals(receipt, items),
|
|
759
|
+
...pdf ? { pdf } : {}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function throwIfNotXml(response) {
|
|
763
|
+
throwIfHeaderError(response);
|
|
764
|
+
throwIfTextError(response);
|
|
765
|
+
if (looksLikeXml(response)) return;
|
|
766
|
+
throwIfHttpError(response);
|
|
767
|
+
throw unexpectedResponse(response, "XML választ vártunk.");
|
|
768
|
+
}
|
|
769
|
+
function parseReceiptResponse(response) {
|
|
770
|
+
throwIfNotXml(response);
|
|
771
|
+
const root = parseResponseXml(response);
|
|
772
|
+
throwIfXmlFailure(root, response);
|
|
773
|
+
if (childBoolean(root, "sikeres") !== true) {
|
|
774
|
+
throwIfHttpError(response);
|
|
775
|
+
throw unexpectedResponse(response, "A válasz nem jelez sikeres műveletet.");
|
|
776
|
+
}
|
|
777
|
+
return parseReceiptXml(root, response);
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/receipts/create.ts
|
|
781
|
+
const CREATE_RECEIPT_NAMESPACE = "http://www.szamlazz.hu/xmlnyugtacreate";
|
|
782
|
+
const DEFAULT_CURRENCY = "HUF";
|
|
783
|
+
const DEFAULT_UNIT = "db";
|
|
784
|
+
const PREFIX_PATTERN = /^[A-Z0-9]+$/;
|
|
785
|
+
const AMOUNT_TOLERANCE = 2;
|
|
786
|
+
const MONEY_DECIMALS = 2;
|
|
787
|
+
const receiptOnlyVatCodes = new Set(RECEIPT_ONLY_VAT_CODES);
|
|
788
|
+
function calculateItem(item, currency, index) {
|
|
789
|
+
const vat = typeof item.vat === "string" && receiptOnlyVatCodes.has(item.vat) ? 0 : item.vat;
|
|
790
|
+
try {
|
|
791
|
+
return calculateReceiptItem({
|
|
792
|
+
...item,
|
|
793
|
+
vat
|
|
794
|
+
}, currency);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (!(error instanceof SzamlazzError)) throw error;
|
|
797
|
+
throw receiptValidationError(`${index + 1}. tétel (${item.name}): ${error.message}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function assertHufAmounts(amounts, label, vat) {
|
|
801
|
+
if (!Number.isInteger(amounts.grossAmount)) throw receiptValidationError(`${label}: forintos nyugtán a bruttó összegnek egész számnak kell lennie (363).`);
|
|
802
|
+
if (decimalPlaces(amounts.netAmount) > MONEY_DECIMALS) throw receiptValidationError(`${label}: forintos nyugtán a nettó összeg legfeljebb 2 tizedesjegyet tartalmazhat (364).`);
|
|
803
|
+
if (decimalPlaces(amounts.vatAmount) > MONEY_DECIMALS) throw receiptValidationError(`${label}: forintos nyugtán az áfa összeg legfeljebb 2 tizedesjegyet tartalmazhat (365).`);
|
|
804
|
+
if (roundMoney(amounts.netAmount + amounts.vatAmount, MONEY_DECIMALS) !== amounts.grossAmount) throw receiptValidationError(`${label}: a nettó és az áfa összegének pontosan ki kell adnia a bruttót (261).`);
|
|
805
|
+
if (Math.abs(amounts.netUnitPrice * amounts.quantity - amounts.netAmount) > AMOUNT_TOLERANCE) throw receiptValidationError(`${label}: a nettó egységár és a mennyiség szorzata nem egyezik a nettó összeggel (259).`);
|
|
806
|
+
const expectedVat = amounts.netAmount * vatPercentage(vat) / 100;
|
|
807
|
+
if (Math.abs(expectedVat - amounts.vatAmount) > AMOUNT_TOLERANCE) throw receiptValidationError(`${label}: az áfa összeg nem egyezik a nettó összeg és az áfakulcs szorzatával (260).`);
|
|
808
|
+
}
|
|
809
|
+
function calculateReceiptItems(items, currency) {
|
|
810
|
+
if (items.length === 0) throw receiptValidationError("A nyugtán legalább egy tételnek szerepelnie kell.");
|
|
811
|
+
return items.map((input, index) => {
|
|
812
|
+
requireText(input.name, `A(z) ${index + 1}. tétel megnevezése (name) kötelező.`);
|
|
813
|
+
const amounts = calculateItem(input, currency, index);
|
|
814
|
+
if (isHuf(currency)) assertHufAmounts(amounts, `${index + 1}. tétel (${input.name})`, input.vat);
|
|
815
|
+
if (input.dataDeletionCode !== void 0) {
|
|
816
|
+
if (!Number.isInteger(input.dataDeletionCode) || input.dataDeletionCode < 0) throw receiptValidationError(`A(z) ${index + 1}. tétel adattörlő kódja (dataDeletionCode) nemnegatív egész szám legyen.`);
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
input,
|
|
820
|
+
amounts,
|
|
821
|
+
vat: input.vat
|
|
822
|
+
};
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
function formatAmount(value, currency, decimals) {
|
|
826
|
+
return isHuf(currency) ? value.toFixed(decimals) : value;
|
|
827
|
+
}
|
|
828
|
+
function itemNode(item, currency, unit) {
|
|
829
|
+
const { input, amounts } = item;
|
|
830
|
+
const ledger = input.ledger;
|
|
831
|
+
return el("tetel", [
|
|
832
|
+
el("megnevezes", input.name.trim()),
|
|
833
|
+
optionalEl("azonosito", optionalText(input.identifier)),
|
|
834
|
+
el("mennyiseg", amounts.quantity),
|
|
835
|
+
el("mennyisegiEgyseg", optionalText(input.unit) ?? unit),
|
|
836
|
+
el("nettoEgysegar", amounts.netUnitPrice),
|
|
837
|
+
el("afakulcs", String(item.vat)),
|
|
838
|
+
el("netto", formatAmount(amounts.netAmount, currency, MONEY_DECIMALS)),
|
|
839
|
+
el("afa", formatAmount(amounts.vatAmount, currency, MONEY_DECIMALS)),
|
|
840
|
+
el("brutto", formatAmount(amounts.grossAmount, currency, 0)),
|
|
841
|
+
ledger && optionalEl("fokonyv", [optionalEl("arbevetel", optionalText(ledger.revenue)), optionalEl("afa", optionalText(ledger.vat))]),
|
|
842
|
+
optionalEl("megjegyzes", optionalText(input.comment)),
|
|
843
|
+
optionalEl("torloKod", input.dataDeletionCode)
|
|
844
|
+
]);
|
|
845
|
+
}
|
|
846
|
+
function paymentNodes(payments, items) {
|
|
847
|
+
if (payments === void 0 || payments.length === 0) return void 0;
|
|
848
|
+
const nodes = payments.map((payment, index) => {
|
|
849
|
+
const method = requireText(payment.method, `A(z) ${index + 1}. kifizetés fizetési eszköze (method) kötelező.`);
|
|
850
|
+
if (!Number.isFinite(payment.amount)) throw receiptValidationError(`A(z) ${index + 1}. kifizetés összege nem érvényes szám.`);
|
|
851
|
+
return el("kifizetes", [
|
|
852
|
+
el("fizetoeszkoz", method),
|
|
853
|
+
el("osszeg", payment.amount),
|
|
854
|
+
optionalEl("leiras", optionalText(payment.description))
|
|
855
|
+
]);
|
|
856
|
+
});
|
|
857
|
+
const paid = roundMoney(payments.reduce((sum, payment) => sum + payment.amount, 0), MONEY_DECIMALS);
|
|
858
|
+
const gross = roundMoney(items.reduce((sum, item) => sum + item.amounts.grossAmount, 0), MONEY_DECIMALS);
|
|
859
|
+
if (paid !== gross) throw receiptValidationError(`A kifizetések összege (${paid}) eltér a nyugta bruttó végösszegétől (${gross}) (340).`);
|
|
860
|
+
return el("kifizetesek", nodes);
|
|
861
|
+
}
|
|
862
|
+
function resolvePrefix(value) {
|
|
863
|
+
const prefix = requireText(value, "Add meg a nyugta előtagját (prefix) a hívásban vagy a receipts alapbeállításaiban.");
|
|
864
|
+
if (!PREFIX_PATTERN.test(prefix)) throw receiptValidationError(`A nyugta előtag csak nagybetűt és számot tartalmazhat, kapott: ${prefix} (337).`);
|
|
865
|
+
return prefix;
|
|
866
|
+
}
|
|
867
|
+
function resolveExchange(currency, rate, bank) {
|
|
868
|
+
if (isHuf(currency)) return {
|
|
869
|
+
rate: void 0,
|
|
870
|
+
bank: void 0
|
|
871
|
+
};
|
|
872
|
+
if (rate === void 0 || !Number.isFinite(rate) || rate <= 0) throw receiptValidationError(`Devizás nyugtánál (${currency}) add meg az árfolyamot (exchangeRate) pozitív számként.`);
|
|
873
|
+
return {
|
|
874
|
+
rate,
|
|
875
|
+
bank: requireText(bank, `Devizás nyugtánál (${currency}) add meg az árfolyamot jegyző bankot (exchangeBank).`)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function buildCreateReceiptXml(credentials, defaults, input) {
|
|
879
|
+
const prefix = resolvePrefix(input.prefix ?? defaults.prefix);
|
|
880
|
+
const paymentMethod = requireText(input.paymentMethod ?? defaults.paymentMethod, "Add meg a fizetési módot (paymentMethod) a hívásban vagy a receipts alapbeállításaiban.");
|
|
881
|
+
const currency = optionalText(input.currency ?? defaults.currency) ?? DEFAULT_CURRENCY;
|
|
882
|
+
const exchange = resolveExchange(currency, input.exchangeRate ?? defaults.exchangeRate, input.exchangeBank ?? defaults.exchangeBank);
|
|
883
|
+
const template = validateTemplate(input.template ?? defaults.template);
|
|
884
|
+
const items = calculateReceiptItems(input.items ?? [], currency);
|
|
885
|
+
const unit = optionalText(defaults.unit) ?? DEFAULT_UNIT;
|
|
886
|
+
const payments = paymentNodes(input.payments, items);
|
|
887
|
+
const downloadPdf = input.downloadPdf ?? defaults.downloadPdf ?? true;
|
|
888
|
+
return buildXmlDocument({
|
|
889
|
+
root: "xmlnyugtacreate",
|
|
890
|
+
namespace: CREATE_RECEIPT_NAMESPACE,
|
|
891
|
+
schemaLocation: `${RECEIPT_XSD_BASE_URL}nyugtacreate/xmlnyugtacreate.xsd`,
|
|
892
|
+
children: [
|
|
893
|
+
el("beallitasok", [...credentials, el("pdfLetoltes", downloadPdf)]),
|
|
894
|
+
el("fejlec", [
|
|
895
|
+
optionalEl("hivasAzonosito", optionalText(input.callId)),
|
|
896
|
+
el("elotag", prefix),
|
|
897
|
+
el("fizmod", paymentMethod),
|
|
898
|
+
el("penznem", currency),
|
|
899
|
+
optionalEl("devizaarf", exchange.rate),
|
|
900
|
+
optionalEl("devizabank", exchange.bank),
|
|
901
|
+
optionalEl("megjegyzes", optionalText(input.comment)),
|
|
902
|
+
optionalEl("pdfSablon", template),
|
|
903
|
+
optionalEl("fokonyvVevo", optionalText(input.customerLedgerId ?? defaults.customerLedgerId)),
|
|
904
|
+
optionalEl("rendelesSzam", optionalText(input.orderNumber))
|
|
905
|
+
]),
|
|
906
|
+
el("tetelek", items.map((item) => itemNode(item, currency, unit))),
|
|
907
|
+
payments
|
|
908
|
+
]
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const DUPLICATE_CALL_ID_CODE = 338;
|
|
912
|
+
function enrichDuplicateError(error, input) {
|
|
913
|
+
if (!(error instanceof SzamlazzError) || error.code !== DUPLICATE_CALL_ID_CODE) return error;
|
|
914
|
+
const lookup = input.orderNumber ? ` A meglévő nyugtát a getReceipt({ orderNumber: '${input.orderNumber}' }) hívással kérdezheted le, ha a fiókban tiltva van a rendelésszám ismétlődése.` : " A Számla Agent hívásazonosító alapján nem ad lekérdezési lehetőséget, ezért a meglévő nyugtát a Számlázz.hu felületén keresd meg, vagy adj meg rendelésszámot (orderNumber) is.";
|
|
915
|
+
return new SzamlazzError(error.message, {
|
|
916
|
+
category: "duplicate",
|
|
917
|
+
code: error.code,
|
|
918
|
+
hint: `Ezzel a hívásazonosítóval (callId: ${input.callId ?? ""}) már készült nyugta, a nyugta nem jött létre újra.${lookup}`,
|
|
919
|
+
action: error.action,
|
|
920
|
+
httpStatus: error.httpStatus,
|
|
921
|
+
rawResponse: error.rawResponse,
|
|
922
|
+
cause: error
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
async function createReceipt(ctx, defaults, input, options = {}) {
|
|
926
|
+
const xml = buildCreateReceiptXml(ctx.credentials, defaults, input);
|
|
927
|
+
try {
|
|
928
|
+
return await ctx.execute({
|
|
929
|
+
action: "createReceipt",
|
|
930
|
+
xml,
|
|
931
|
+
signal: options.signal,
|
|
932
|
+
safeToRetry: optionalText(input.callId) !== void 0
|
|
933
|
+
}, parseReceiptResponse);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
throw enrichDuplicateError(error, input);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
//#endregion
|
|
939
|
+
export { childBoolean as A, readHeader as C, throwIfTextError as D, throwIfHttpError as E, AGENT_ACTIONS as F, MAX_INVOICE_ATTACHMENTS as I, SZAMLAZZ_AGENT_URL as L, childText as M, findChild as N, throwIfXmlFailure as O, findChildren as P, readDocumentHeaders as S, throwIfHeaderError as T, optionalEl as _, RECEIPT_XSD_BASE_URL as a, parseDoneText as b, requireReceiptNumber as c, presentText as d, resolveInvoice as f, el as g, buildXmlDocument as h, parseReceiptResponse as i, childNumber as j, unexpectedResponse as k, validateTemplate as l, TAXPAYER_TYPE_CODES as m, calculateReceiptItems as n, optionalText as o, INVOICE_TEMPLATES as p, createReceipt as r, receiptValidationError as s, buildCreateReceiptXml as t, optionalAgentDate as u, createAgentResponse as v, throwIfAnyError as w, parseResponseXml as x, looksLikeXml as y };
|