kassza 0.13.0 → 0.14.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 (164) hide show
  1. package/README.en.md +48 -0
  2. package/README.md +152 -22
  3. package/agents/README.md +6 -1
  4. package/agents/api.md +64 -12
  5. package/agents/pitfalls.md +12 -1
  6. package/agents/recipes.md +67 -8
  7. package/agents/skills/kassza/SKILL.md +3 -0
  8. package/agents/skills/migrate-from-szamlazz-js/SKILL.md +39 -0
  9. package/dist/batch/index.cjs +346 -0
  10. package/dist/batch/index.d.cts +91 -0
  11. package/dist/batch/index.d.ts +91 -0
  12. package/dist/batch/index.js +339 -0
  13. package/dist/cli.cjs +105 -14
  14. package/dist/cli.js +104 -13
  15. package/dist/{client-C-PdfL_P.cjs → client-CLFvJCVW.cjs} +358 -181
  16. package/dist/{client-CfaS1-Jx.js → client-Cvf-hTz_.js} +311 -152
  17. package/dist/{client-6MkeuFqD.d.ts → client-DeWWQ-RZ.d.ts} +99 -26
  18. package/dist/{client-B5FryKxJ.d.cts → client-SVZu25MB.d.cts} +99 -26
  19. package/dist/{context-DXNbTPwz.d.cts → context-CFNKzStE.d.cts} +82 -58
  20. package/dist/{context-BxQlIlug.d.ts → context-kb33TgzE.d.ts} +82 -58
  21. package/dist/cookie-stores/index.cjs +21 -150
  22. package/dist/cookie-stores/index.d.cts +4 -63
  23. package/dist/cookie-stores/index.d.ts +4 -63
  24. package/dist/cookie-stores/index.js +3 -144
  25. package/dist/create-CflXrPip.js +204 -0
  26. package/dist/create-CvUvLUaT.cjs +257 -0
  27. package/dist/{create-once-DgMtKKcG.cjs → create-once-9yxR_Er_.cjs} +285 -33
  28. package/dist/create-once-Bh6fOWkW.d.cts +20 -0
  29. package/dist/{create-once-Cz621l8r.js → create-once-Ca2gMk1h.js} +250 -34
  30. package/dist/create-once-VKp0tVow.d.ts +20 -0
  31. package/dist/data-link/index.cjs +4 -4
  32. package/dist/data-link/index.d.cts +2 -52
  33. package/dist/data-link/index.d.ts +2 -52
  34. package/dist/data-link/index.js +4 -4
  35. package/dist/delegation/index.cjs +4 -4
  36. package/dist/delegation/index.d.cts +2 -2
  37. package/dist/delegation/index.d.ts +2 -2
  38. package/dist/delegation/index.js +4 -4
  39. package/dist/{errors-DLrb0g64.js → errors-DtN4g5Z9.js} +9 -1
  40. package/dist/{errors-DxeuTslP.cjs → errors-EE96_6D_.cjs} +20 -0
  41. package/dist/{get-Csm2n0rZ.cjs → get-BdMTseh4.cjs} +9 -183
  42. package/dist/{get-DT3JEgQ6.js → get-CZEShCcj.js} +4 -124
  43. package/dist/index-BhUPY_EH.d.cts +109 -0
  44. package/dist/index-BqGgQ4t3.d.ts +109 -0
  45. package/dist/{index-CYAGCdKJ.d.cts → index-C-7yWV7L.d.cts} +24 -1
  46. package/dist/{index-CYAGCdKJ.d.ts → index-C-7yWV7L.d.ts} +24 -1
  47. package/dist/index.cjs +19 -5
  48. package/dist/index.d.cts +10 -20
  49. package/dist/index.d.ts +10 -20
  50. package/dist/index.js +6 -6
  51. package/dist/ipn/index.cjs +26 -5
  52. package/dist/ipn/index.d.cts +12 -1
  53. package/dist/ipn/index.d.ts +12 -1
  54. package/dist/ipn/index.js +26 -6
  55. package/dist/{issue-AGRohSm7.cjs → issue-Bin4tKD_.cjs} +416 -342
  56. package/dist/{issue-DtSLpVdJ.js → issue-DMA1Nbr1.js} +394 -266
  57. package/dist/journal/index.cjs +454 -0
  58. package/dist/journal/index.d.cts +18 -0
  59. package/dist/journal/index.d.ts +18 -0
  60. package/dist/journal/index.js +445 -0
  61. package/dist/journal-DvI81Kt7.d.cts +114 -0
  62. package/dist/journal-Y9qrIB8G.d.ts +114 -0
  63. package/dist/mcp/index.cjs +1 -1
  64. package/dist/mcp/index.d.cts +1 -1
  65. package/dist/mcp/index.d.ts +1 -1
  66. package/dist/mcp/index.js +1 -1
  67. package/dist/money/index.cjs +2 -1
  68. package/dist/money/index.d.cts +2 -2
  69. package/dist/money/index.d.ts +2 -2
  70. package/dist/money/index.js +2 -2
  71. package/dist/{money-t7uy1n91.js → money-Cw457SD6.js} +95 -3
  72. package/dist/{money-BxytJW6f.cjs → money-K9j0wPit.cjs} +99 -1
  73. package/dist/nav/index.cjs +2 -2
  74. package/dist/nav/index.d.cts +1 -1
  75. package/dist/nav/index.d.ts +1 -1
  76. package/dist/nav/index.js +2 -2
  77. package/dist/{nav-BEwcX7-N.cjs → nav-BNbc8U0R.cjs} +1 -1
  78. package/dist/{nav-CK1WgYo7.js → nav-BcCerwWS.js} +1 -1
  79. package/dist/node/index.cjs +124 -0
  80. package/dist/node/index.d.cts +35 -0
  81. package/dist/node/index.d.ts +35 -0
  82. package/dist/node/index.js +120 -0
  83. package/dist/observe/index.cjs +311 -0
  84. package/dist/observe/index.d.cts +53 -0
  85. package/dist/observe/index.d.ts +53 -0
  86. package/dist/observe/index.js +303 -0
  87. package/dist/once-BN_3B5OF.cjs +103 -0
  88. package/dist/once-CqqZxsXG.js +74 -0
  89. package/dist/{parse-C608ehqM.js → parse-Cr7yQ3_z.js} +2 -2
  90. package/dist/{parse-BJj-aqKy.cjs → parse-DOA2XQ3e.cjs} +2 -2
  91. package/dist/payments/barion.cjs +26 -2
  92. package/dist/payments/barion.d.cts +2 -2
  93. package/dist/payments/barion.d.ts +2 -2
  94. package/dist/payments/barion.js +26 -2
  95. package/dist/payments/index.cjs +7 -2
  96. package/dist/payments/index.d.cts +4 -4
  97. package/dist/payments/index.d.ts +4 -4
  98. package/dist/payments/index.js +3 -3
  99. package/dist/payments/paypal.cjs +16 -2
  100. package/dist/payments/paypal.d.cts +2 -2
  101. package/dist/payments/paypal.d.ts +2 -2
  102. package/dist/payments/paypal.js +16 -2
  103. package/dist/payments/revolut.cjs +16 -2
  104. package/dist/payments/revolut.d.cts +2 -2
  105. package/dist/payments/revolut.d.ts +2 -2
  106. package/dist/payments/revolut.js +16 -2
  107. package/dist/payments/simplepay.cjs +36 -4
  108. package/dist/payments/simplepay.d.cts +2 -2
  109. package/dist/payments/simplepay.d.ts +2 -2
  110. package/dist/payments/simplepay.js +36 -4
  111. package/dist/payments/stripe.cjs +19 -2
  112. package/dist/payments/stripe.d.cts +2 -2
  113. package/dist/payments/stripe.d.ts +2 -2
  114. package/dist/payments/stripe.js +19 -2
  115. package/dist/push-BXj0Sf0Z.d.ts +52 -0
  116. package/dist/push-D4nNbCNV.d.cts +52 -0
  117. package/dist/reference-B5TATqb5.js +124 -0
  118. package/dist/reference-BIdYMK0t.cjs +183 -0
  119. package/dist/reports/index.cjs +1 -1
  120. package/dist/reports/index.d.cts +1 -1
  121. package/dist/reports/index.d.ts +1 -1
  122. package/dist/reports/index.js +1 -1
  123. package/dist/{serialize-DSE_o9RK.js → serialize-B2eFh3D-.js} +5 -1
  124. package/dist/{serialize-BXOww1Uq.cjs → serialize-CquIsxA-.cjs} +5 -1
  125. package/dist/{server-Cd8HB661.js → server-BrJUbXs7.js} +8 -7
  126. package/dist/{server-C6PoNkdD.cjs → server-Dw9j8Mnd.cjs} +17 -16
  127. package/dist/session-Ckl5eJhV.d.ts +7 -0
  128. package/dist/{session-CKOuE6Jc.cjs → session-DeM5vjvC.cjs} +69 -13
  129. package/dist/{session-rMKaJcmq.js → session-SaEEXu2N.js} +58 -14
  130. package/dist/session-YbChChx7.d.cts +7 -0
  131. package/dist/storage/fs.cjs +1 -1
  132. package/dist/storage/fs.js +1 -1
  133. package/dist/storage/index.cjs +1 -1
  134. package/dist/storage/index.js +1 -1
  135. package/dist/store-C1rHd9DB.d.cts +19 -0
  136. package/dist/store-C1rHd9DB.d.ts +19 -0
  137. package/dist/stores/index.cjs +24 -0
  138. package/dist/stores/index.d.cts +4 -0
  139. package/dist/stores/index.d.ts +4 -0
  140. package/dist/stores/index.js +3 -0
  141. package/dist/stores-BNrS0dWl.cjs +506 -0
  142. package/dist/stores-DhjGBrl_.js +441 -0
  143. package/dist/testing/index.cjs +28 -16
  144. package/dist/testing/index.d.cts +6 -3
  145. package/dist/testing/index.d.ts +6 -3
  146. package/dist/testing/index.js +26 -14
  147. package/dist/{types-BVeR7KyU.d.ts → types-B1A8mUIE.d.cts} +13 -1
  148. package/dist/{types-BVeR7KyU.d.cts → types-ByAEvGsZ.d.ts} +13 -1
  149. package/dist/{types-k--zHf4l.d.ts → types-D1zDOH_F.d.ts} +1 -1
  150. package/dist/{types-DH8QoNZK.d.cts → types-lnKkQdot.d.cts} +1 -1
  151. package/dist/warnings-B3DXnveC.d.cts +31 -0
  152. package/dist/warnings-B3DXnveC.d.ts +31 -0
  153. package/dist/warnings-DeIw5RFj.cjs +20 -0
  154. package/dist/warnings-DwTl5J-2.js +15 -0
  155. package/dist/{webhook-CrEGohbg.js → webhook-CQVShAfI.js} +44 -1
  156. package/dist/{webhook-81giRMdQ.cjs → webhook-Ckj-_hPD.cjs} +67 -0
  157. package/dist/webhook-CmdhGiH5.d.cts +11 -0
  158. package/dist/webhook-D6lEejob.d.ts +11 -0
  159. package/llms.txt +4 -3
  160. package/package.json +92 -4
  161. package/dist/session-Dm_45x8K.d.cts +0 -11
  162. package/dist/session-Dm_45x8K.d.ts +0 -11
  163. package/dist/webhook-CpCMHhn2.d.ts +0 -6
  164. package/dist/webhook-CpMJuP6c.d.cts +0 -6
package/README.en.md ADDED
@@ -0,0 +1,48 @@
1
+ # kassza (English overview)
2
+
3
+ kassza is an unofficial, zero-dependency TypeScript client for the [Számlázz.hu](https://www.szamlazz.hu) Számla Agent API, the most widely used Hungarian invoicing service. The full documentation is in Hungarian at [kasszajs.hu](https://kasszajs.hu), because the domain (Hungarian VAT, NAV reporting, receipt rules) is Hungarian. This page is a short entry point for developers who do not read Hungarian.
4
+
5
+ ```bash
6
+ npm install kassza
7
+ ```
8
+
9
+ ```ts
10
+ import { createKassza } from 'kassza'
11
+
12
+ const kassza = createKassza() // reads SZAMLAZZ_AGENT_KEY
13
+
14
+ const { number, created } = await kassza.invoices.createOnce({
15
+ orderNumber: 'WEB-1042',
16
+ paid: true,
17
+ buyer: { name: 'Buyer Ltd.', zip: '1111', city: 'Budapest', address: 'Fő utca 1.' },
18
+ items: [{ name: 'Subscription', grossUnitPrice: 12_700, vat: 27 }],
19
+ })
20
+ ```
21
+
22
+ ## What it does
23
+
24
+ - Invoices, proformas, advance, final and corrective invoices, delivery notes, reversals, payments, PDFs, invoice data, taxpayer lookup.
25
+ - Receipts (nyugta): create, reverse, query, email, and convert to an invoice later.
26
+ - Exactly-once issuing: `createOnce` looks the document up before creating it and after uncertain failures; concurrent calls are merged, and a shared lock (`createOnceLock`) prevents duplicates across serverless instances.
27
+ - Payment providers: Stripe, SimplePay, Barion, Revolut and PayPal webhooks that issue a receipt or invoice exactly once, reverse it on a full refund, and issue corrective invoices on partial refunds.
28
+ - NAV (Hungarian tax authority) daily receipt reports, reconciliation, and a document journal to build them from.
29
+ - Bulk and recurring invoicing with rate limiting, a fake Számla Agent for tests, Express/NestJS adapter, observability (logs, tracing, Prometheus metrics), a CLI (`npx kassza doctor`) and an MCP server.
30
+ - Runs on Node.js, Bun, Deno, Cloudflare Workers and edge runtimes; CI tests all four on the built package.
31
+ - Sends no telemetry.
32
+
33
+ ## Rules that matter
34
+
35
+ 1. Never retry creating an invoice in a loop: Számlázz.hu bans accounts that do. kassza retries only safe lookups.
36
+ 2. Always set an `orderNumber`, and prefer `createOnce`.
37
+ 3. Give prices as `netUnitPrice` or `grossUnitPrice` with a VAT rate; kassza applies the official rounding rules.
38
+ 4. Errors are `SzamlazzError` with a `category` (`validation`, `duplicate`, `network`, …) and a `docsUrl` to a page about the error code.
39
+
40
+ ## For AI coding agents
41
+
42
+ English docs for agents ship inside the package: [agents/README.md](agents/README.md), [pitfalls](agents/pitfalls.md), [API reference](agents/api.md), [recipes](agents/recipes.md), and [llms.txt](llms.txt).
43
+
44
+ ## Status
45
+
46
+ kassza is in beta (0.x). The API may still change between minor versions; see the [changelog](CHANGELOG.md) (in Hungarian). It is not affiliated with Számlázz.hu.
47
+
48
+ License: MIT.
package/README.md CHANGED
@@ -6,7 +6,8 @@
6
6
 
7
7
  <p align="center">
8
8
  <b>Számlázz.hu, TypeScriptben, egyszerűbben.</b><br>
9
- Nem hivatalos TypeScript wrapper a Számlázz.hu Számla Agenthez. Mind a 11 Agent művelet, 0 függőség, nulla runtime kompromisszum.
9
+ Nem hivatalos TypeScript wrapper a Számlázz.hu Számla Agenthez. Mind a 11 Agent művelet, 0 függőség, nulla runtime kompromisszum.<br>
10
+ <sub><a href="https://github.com/futozs/kassza/blob/main/README.en.md">English overview</a></sub>
10
11
  </p>
11
12
 
12
13
  <p align="center">
@@ -14,14 +15,16 @@
14
15
  <a href="https://www.npmjs.com/package/kassza"><img src="https://img.shields.io/npm/dm/kassza?label=let%C3%B6lt%C3%A9s&style=flat-square&labelColor=28313d&color=c2501a" alt="havi letöltés"></a>
15
16
  <a href="https://github.com/futozs/kassza/blob/main/package.json"><img src="https://img.shields.io/badge/f%C3%BCgg%C5%91s%C3%A9g-0-c2501a?style=flat-square&labelColor=28313d" alt="0 futásidejű függőség"></a>
16
17
  <a href="https://github.com/futozs/kassza/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/kassza?label=licenc&style=flat-square&labelColor=28313d&color=c2501a" alt="MIT licenc"></a>
17
- </p>
18
-
19
- <p align="center">
20
- <a href="https://kasszajs.hu"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-web-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-web-light.svg" alt="Weboldal: kasszajs.hu" width="428"></picture></a>
21
- <a href="https://kasszajs.hu/docs"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-docs-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-docs-light.svg" alt="Dokumentáció: 113 oldal, magyarul" width="428"></picture></a>
22
18
  <br>
23
- <a href="https://kasszajs.hu/sandbox"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-sandbox-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-sandbox-light.svg" alt="Sandbox: 28 futtatható példa" width="428"></picture></a>
24
- <a href="https://kasszajs.hu/docs/receptek"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-recipes-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/nav-recipes-light.svg" alt="Receptek: 11 kész integráció" width="428"></picture></a>
19
+ <a href="https://github.com/futozs/kassza/actions/workflows/ci.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/futozs/kassza/ci.yml?branch=main&label=CI&style=flat-square&labelColor=28313d" alt="CI állapota"></a>
20
+ <a href="https://github.com/futozs/kassza/actions/workflows/full-check.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/futozs/kassza/full-check.yml?branch=main&label=heti%20teljes%20teszt&style=flat-square&labelColor=28313d" alt="Heti teljes ellenőrzés: CI, XSD, változásfigyelő, élő e2e"></a>
21
+ <a href="https://github.com/futozs/kassza/actions/workflows/release.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/futozs/kassza/release.yml?branch=main&label=kiad%C3%A1s&style=flat-square&labelColor=28313d" alt="Kiadás állapota"></a>
22
+ <a href="https://scorecard.dev/viewer/?uri=github.com/futozs/kassza"><img src="https://img.shields.io/ossf-scorecard/github.com/futozs/kassza?label=OpenSSF%20Scorecard&style=flat-square&labelColor=28313d" alt="OpenSSF Scorecard pontszám"></a>
23
+ <br>
24
+ <a href="https://github.com/futozs/kassza/actions/workflows/badges.yml"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffutozs%2Fkassza%2Fbadges%2Fcoverage-lines.json&style=flat-square&labelColor=28313d" alt="Sor-lefedettség"></a>
25
+ <a href="https://github.com/futozs/kassza/actions/workflows/badges.yml"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffutozs%2Fkassza%2Fbadges%2Fcoverage-branches.json&style=flat-square&labelColor=28313d" alt="Ág-lefedettség"></a>
26
+ <a href="https://github.com/futozs/kassza/actions/workflows/badges.yml"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffutozs%2Fkassza%2Fbadges%2Fcoverage-functions.json&style=flat-square&labelColor=28313d" alt="Függvény-lefedettség"></a>
27
+ <a href="https://github.com/futozs/kassza/actions/workflows/badges.yml"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffutozs%2Fkassza%2Fbadges%2Fcoverage-statements.json&style=flat-square&labelColor=28313d" alt="Utasítás-lefedettség"></a>
25
28
  </p>
26
29
 
27
30
  ```bash
@@ -96,7 +99,8 @@ Ennyi. A kerekítést, a magyar dátumot, az XML-t, a session cookie-t és a hib
96
99
  | ⚙️ [Beállítás](#beállítás) | 🧾 [Nyugták](#nyugták) | 📊 [NAV nyugta-adatszolgáltatás](#nav-nyugta-adatszolgáltatás) | ⚡ [Serverless és edge](#serverless-és-edge) |
97
100
  | 🚨 [Hibakezelés](#hibakezelés) | 🏛️ [Adószám lekérdezés](#adószám-lekérdezés) | 🤝 [Megbízotti számlázás](#megbízotti-számlázás) | 🧪 [Tesztelés](#tesztelés) |
98
101
  | 🎛️ [Haladó beállítások](#haladó-beállítások) | 🔔 [Fizetési értesítés (IPN)](#fizetési-értesítés-ipn) | 🔗 [Pénzügyi adatkapcsolat](#pénzügyi-adatkapcsolat) | 🧮 [Validátorok és pénzszámítás](#validátorok-és-pénzszámítás) |
99
- | 🤖 [AI-val kódolsz?](#ai-val-kódolsz) | | | 💻 [Parancssor és MCP szerver](#parancssor-és-mcp-szerver) |
102
+ | 🤖 [AI-val kódolsz?](#ai-val-kódolsz) | 🔁 [Tömeges számlázás](#tömeges-és-ismétlődő-számlázás) | 🟩 [Node.js és Express](#nodejs-és-express) | 💻 [Parancssor és MCP szerver](#parancssor-és-mcp-szerver) |
103
+ | | | 📒 [Bizonylatnapló](#bizonylatnapló) | 📈 [Megfigyelhetőség](#megfigyelhetőség) |
100
104
 
101
105
  ## Weboldal, sandbox és receptek
102
106
 
@@ -106,7 +110,7 @@ A teljes magyar dokumentáció, a sandbox és a receptek a **[kasszajs.hu](https
106
110
  <a href="https://kasszajs.hu/sandbox?pelda=szamla"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/showcase-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/showcase-light.svg" alt="A kassza sandbox: futtatható példák, TypeScript kód és a ténylegesen elküldött XML" width="100%"></picture></a>
107
111
  </p>
108
112
 
109
- - 📖 **[Dokumentáció](https://kasszajs.hu/docs)**: 113 oldal magyarul. Minden Agent művelethez kérés, válasz és futtatható minta, a beállítások, a szabályok és az összes ismert hibakód.
113
+ - 📖 **[Dokumentáció](https://kasszajs.hu/docs)**: 188 oldal magyarul. Minden Agent művelethez kérés, válasz és futtatható minta, a beállítások, a szabályok és az összes ismert hibakód.
110
114
  - ▶️ **[Sandbox](https://kasszajs.hu/sandbox)**: 28 példa, amit a böngészőben futtathatsz Agent kulcs nélkül, szimulált Számlázz.hu ellen, a ténylegesen elküldött XML-lel. A saját kódodat linkként meg is oszthatod.
111
115
  - 🍳 **[Receptek](https://kasszajs.hu/docs/receptek)**: 11 teljes, bemásolható integráció webshophoz, Stripe és IPN webhookhoz, díjbekérőhöz, pénztári nyugtához, PDF tárhelyhez, Cloudflare Workershez és tesztekhez.
112
116
  - 🤖 **[llms.txt](https://kasszajs.hu/llms.txt)**: a dokumentáció tartalomjegyzéke AI asszisztenseknek, a teljes szöveg egy fájlban: [llms-full.txt](https://kasszajs.hu/llms-full.txt).
@@ -421,6 +425,8 @@ await kassza.invoices.clearPayments('E-WEB-2026-12')
421
425
 
422
426
  A `registerPayment` egyetlen befizetést és több részletet is fogad. A `clearPayments` az összes befizetést törli a számláról.
423
427
 
428
+ Ha ugyanaz a befizetés több helyről is érkezhet (banki értesítés, webhook), a `registerPaymentOnce({ invoiceNumber, key, amount })` egy stabil kulccsal pontosan egyszer rögzíti: a kulcsot jelölőként a befizetés leírásába teszi, és rögzítés előtt megkeresi.
429
+
424
430
  📖 **Dokumentáció:** [Befizetés rögzítése](https://kasszajs.hu/docs/befizetes-rogzitese) &nbsp;·&nbsp; ▶️ **Sandbox:** [Befizetések rögzítése](https://kasszajs.hu/sandbox?pelda=befizetes)
425
431
 
426
432
  </details>
@@ -579,7 +585,7 @@ if (ceg.valid) {
579
585
  }
580
586
  ```
581
587
 
582
- A cégadatok a NAV-tól jönnek, a cím formázva, például `1031 Budapest, Záhony utca 7.`
588
+ A cégadatok a NAV-tól jönnek, a cím formázva, például `1031 Budapest, Záhony utca 7.` Űrlapokhoz a `taxpayerCache: { store }` beállítás közös tárolóban megjegyzi az eredményt, így ugyanaz az adószám csak egyszer megy ki.
583
589
 
584
590
  📖 **Dokumentáció:** [Adószám lekérdezés](https://kasszajs.hu/docs/adoszam-lekerdezes) &nbsp;·&nbsp; ▶️ **Sandbox:** [Adószám lekérdezése](https://kasszajs.hu/sandbox?pelda=adoszam)
585
591
 
@@ -632,6 +638,10 @@ try {
632
638
  | `network` / `timeout` / `maintenance` | Átmeneti hiba |
633
639
  | `rate_limit` | A tesztfiókban túl sok bizonylat készült rövid idő alatt (167) |
634
640
  | `attempt_limit` | Ezt a kérést már ötször sikertelenül küldték el, ezért a kassza el sem küldi |
641
+ | `in_progress` | Egy másik folyamat éppen ugyanezt a bizonylatot állítja ki (a `createOnce` zárja) |
642
+ | `store_unavailable` | Egy kötelezőnek beállított tároló (zár, napló) nem érhető el, a kérés el sem ment |
643
+
644
+ Minden ismert hibakódnak saját oldala van; a hiba `docsUrl` mezője oda mutat, így a naplóból egy kattintás.
635
645
 
636
646
  📖 **Dokumentáció:** [Hibakezelés, hibakódok](https://kasszajs.hu/docs/alapok/hibakezeles) &nbsp;·&nbsp; ▶️ **Sandbox:** [Validációs hibák](https://kasszajs.hu/sandbox?pelda=hibakezeles-validacio)
637
647
 
@@ -649,7 +659,16 @@ const { number, created } = await kassza.invoices.createOnce({
649
659
  })
650
660
  ```
651
661
 
652
- Kiállítás előtt megkeresi a számlát, hálózati hiba, időtúllépés vagy 56-os hiba után pedig visszakeresi. Ha a webhook kétszer fut le, a második hívás `created: false` eredménnyel a meglévő számlát adja. Ha ugyanazt a kérést több szerver is küldheti, az `attemptLedger` opció a folyamatok között is betartja az öt próbálkozásos korlátot.
662
+ Kiállítás előtt megkeresi a számlát, hálózati hiba, időtúllépés vagy 56-os hiba után pedig visszakeresi. Ha a webhook kétszer fut le, a második hívás `created: false` eredménnyel a meglévő számlát adja. Az egyszerre érkező, azonos rendelésre szóló hívásokat a kassza összevonja; több szerver vagy serverless példány között a `createOnceLock` közös tároló zárja véd a dupla számla ellen:
663
+
664
+ ```ts
665
+ import { upstashRedisStore } from 'kassza/stores'
666
+
667
+ const store = upstashRedisStore(Redis.fromEnv())
668
+ const kassza = createKassza({ cookieStore: store, attemptLedger: store, createOnceLock: store })
669
+ ```
670
+
671
+ Az `attemptLedger` a folyamatok között is betartja az öt próbálkozásos korlátot, atomikus számlálóval.
653
672
 
654
673
  📖 **Dokumentáció:** [Pontosan egyszer](https://kasszajs.hu/docs/alapok/pontosan-egyszer) &nbsp;·&nbsp; ▶️ **Sandbox:** [Pénztári nyugta, dupla gombnyomás](https://kasszajs.hu/sandbox?pelda=penztari-nyugta)
655
674
 
@@ -726,8 +745,9 @@ export const POST = stripeWebhook({
726
745
  })
727
746
  ```
728
747
 
729
- - Az újraküldött webhook nem állít ki második bizonylatot.
748
+ - Az újraküldött webhook nem állít ki második bizonylatot; a `dedupe: store` opcióval a már feldolgozott eseményt a Számla Agent hívása nélkül nyugtázza.
730
749
  - Teljes visszatérítéskor a kassza sztornózza a bizonylatot.
750
+ - Részleges visszatérítéskor számlánál visszatérítésenként egy helyesbítő számlát állít ki, a visszatérített összeget tételenként pontosan szétosztva; több áfakulcsnál és nyugtánál javaslatot ad.
731
751
  - Ugyanígy működik a `simplePayWebhook`, a `barionWebhook`, a `revolutWebhook` és a `payPalWebhook`.
732
752
 
733
753
  📖 **Dokumentáció:** [Fizetésből bizonylat](https://kasszajs.hu/docs/fizetesek)
@@ -773,6 +793,36 @@ A NAV kliens alapból csak olvas: a Számlázz.hu által jelentett nyugtákat ne
773
793
  <a href="https://kasszajs.hu/docs/nav-nyugta"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-light.svg" alt="Dokumentáció: NAV nyugta-adatszolgáltatás" height="44"></picture></a>
774
794
  </p>
775
795
 
796
+ ## Bizonylatnapló
797
+
798
+ A NAV napi összesítő és a napi zárás a saját nyilvántartásodból számol, mert a Számla Agent nem listáz nyugtákat dátum szerint. A `kassza/journal` ezt a nyilvántartást adja.
799
+
800
+ <details>
801
+ <summary><b>Napló a NAV összesítőhöz</b> · <code>createJournal()</code>, <code>kvJournal()</code></summary>
802
+
803
+ ```ts
804
+ import { createJournal, kvJournal } from 'kassza/journal'
805
+ import { navDailyReports } from 'kassza/reports'
806
+
807
+ const journal = createJournal(kvJournal(store))
808
+ const kassza = createKassza({
809
+ hooks: { onDocument: (event) => journal.record(event), onDocumentError: 'throw' },
810
+ })
811
+
812
+ const jelentesek = navDailyReports(await journal.receipts({ from: '2026-10-01', to: '2026-10-31' }))
813
+ ```
814
+
815
+ - A `trackReceipt()` és a `trackInvoice()` a kiállítás előtt befejezetlen tételt rögzít; ha a folyamat közben leáll, a `settle()` később visszakeresi és lezárja.
816
+ - A `reconcile()` az adatkapcsolat napi nyugtaarchívumával egyeztet, és pótolja a hiányzó tételeket.
817
+
818
+ 📖 **Dokumentáció:** [Bizonylatnapló](https://kasszajs.hu/docs/kiegeszitok/bizonylatnaplo)
819
+
820
+ </details>
821
+
822
+ <p>
823
+ <a href="https://kasszajs.hu/docs/nav-nyugta/hatarido"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-light.svg" alt="Dokumentáció: Mi a teendő december 31. előtt?" height="44"></picture></a>
824
+ </p>
825
+
776
826
  ## Megbízotti számlázás
777
827
 
778
828
  Platformoknak, amelyek sok cég nevében állítanak ki bizonylatot, a cégek saját Számlázz.hu fiókjában.
@@ -896,13 +946,13 @@ További adapterek: `s3Storage` (AWS SDK), `r2BindingStorage`, `vercelBlobStorag
896
946
  A session cookie újrahasznosítása gyorsítja a hívásokat. Serverless környezetben tedd közös tárolóba.
897
947
 
898
948
  <details>
899
- <summary><b>Upstash Redis</b> · <code>upstashRedisCookieStore()</code></summary>
949
+ <summary><b>Upstash Redis</b> · <code>upstashRedisStore()</code></summary>
900
950
 
901
951
  ```ts
902
952
  import { Redis } from '@upstash/redis'
903
- import { upstashRedisCookieStore } from 'kassza/cookie-stores'
953
+ import { upstashRedisStore } from 'kassza/stores'
904
954
 
905
- const kassza = createKassza({ cookieStore: upstashRedisCookieStore(Redis.fromEnv()) })
955
+ const kassza = createKassza({ cookieStore: upstashRedisStore(Redis.fromEnv()) })
906
956
  ```
907
957
 
908
958
  📖 **Dokumentáció:** [Munkamenet (session cookie)](https://kasszajs.hu/docs/alapok/munkamenet) &nbsp;·&nbsp; ▶️ **Sandbox:** [Közös session több kliens között](https://kasszajs.hu/sandbox?pelda=munkamenet)
@@ -910,19 +960,23 @@ const kassza = createKassza({ cookieStore: upstashRedisCookieStore(Redis.fromEnv
910
960
  </details>
911
961
 
912
962
  <details>
913
- <summary><b>Cloudflare Workers</b> · <code>cloudflareKvCookieStore()</code></summary>
963
+ <summary><b>Cloudflare Workers</b> · <code>cloudflareKvStore()</code>, <code>durableObjectStore()</code></summary>
914
964
 
915
965
  ```ts
916
- import { cloudflareKvCookieStore } from 'kassza/cookie-stores'
966
+ import { cloudflareKvStore } from 'kassza/stores'
917
967
 
918
- const kassza = createKassza({ agentKey: env.SZAMLAZZ_AGENT_KEY, cookieStore: cloudflareKvCookieStore(env.KASSZA_KV) })
968
+ const kassza = createKassza({ agentKey: env.SZAMLAZZ_AGENT_KEY, cookieStore: cloudflareKvStore(env.KASSZA_KV) })
919
969
  ```
920
970
 
971
+ A KV munkamenetnek jó, de nincs benne atomikus írás: a `createOnce` zárjához és a naplóhoz Workers alatt a `durableObjectStore()` kell.
972
+
921
973
  📖 **Dokumentáció:** [Serverless és edge](https://kasszajs.hu/docs/kiegeszitok/serverless-es-edge)
922
974
 
923
975
  </details>
924
976
 
925
- Van még `ioredisCookieStore`, `nodeRedisCookieStore` és `customCookieStore` is. Ha a tároló elérhetetlen, a számlázás attól még működik.
977
+ Van még `ioredisStore`, `nodeRedisStore` és `customStore` is (a régi `kassza/cookie-stores` nevek is működnek). A `diagnoseStore(store)` megmondja, mire alkalmas egy tároló. Ha a tároló elérhetetlen, a számlázás attól még működik.
978
+
979
+ A csomag minden futtatókörnyezetben ugyanazt adja: a CI a buildelt csomagot Node.js, Bun, Deno és Cloudflare workerd alatt is lefuttatja.
926
980
 
927
981
  <p>
928
982
  <a href="https://kasszajs.hu/docs/kiegeszitok/serverless-es-edge"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-light.svg" alt="Dokumentáció: Serverless és edge" height="44"></picture></a>
@@ -1090,7 +1144,8 @@ const kassza = createKassza({
1090
1144
  })
1091
1145
  ```
1092
1146
 
1093
- - Az `onDocument` minden kiállított és sztornózott bizonylat, valamint rögzített befizetés után lefut. Ha hibát dob, a bizonylat attól még elkészült.
1147
+ - Az `onDocument` minden kiállított és sztornózott bizonylat, valamint rögzített befizetés után lefut. Ha hibát dob, a bizonylat attól még elkészült; az `onDocumentError: 'throw'` beállítással ilyenkor `DocumentHookError` jön, benne a bizonylattal.
1148
+ - Az `onWarning` megkapja a korábban csendben elnyelt hibákat (tároló, hook, zár).
1094
1149
  - A `maintenanceCooldownMs` karbantartási hiba után ennyi ideig nem küld kérést, hanem azonnal `maintenance` hibát ad, így átválthatsz tartalék folyamatra.
1095
1150
 
1096
1151
  📖 **Dokumentáció:** [Pontosan egyszer](https://kasszajs.hu/docs/alapok/pontosan-egyszer)
@@ -1102,6 +1157,73 @@ const kassza = createKassza({
1102
1157
  <a href="https://kasszajs.hu/sandbox?pelda=hookok"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-sandbox-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-sandbox-light.svg" alt="Futtasd a sandboxban: Hookok és újrapróbálás" height="44"></picture></a>
1103
1158
  </p>
1104
1159
 
1160
+ ## Megfigyelhetőség
1161
+
1162
+ A kassza nem küld haza semmit. A saját rendszeredben a `kassza/observe` ad strukturált naplót, OpenTelemetry-kompatibilis tracinget és Prometheus metrikákat, függőség nélkül és titokmentesen.
1163
+
1164
+ <details>
1165
+ <summary><b>Napló, tracing, metrikák</b> · <code>observe()</code>, <code>createMetricsRegistry()</code></summary>
1166
+
1167
+ ```ts
1168
+ import { combineHooks, createMetricsRegistry, observe } from 'kassza/observe'
1169
+
1170
+ const metrics = createMetricsRegistry()
1171
+ const kassza = createKassza({ hooks: combineHooks(observe({ logger: console, metrics }), sajatHookok) })
1172
+
1173
+ metrics.renderPrometheus()
1174
+ ```
1175
+
1176
+ 📖 **Dokumentáció:** [Megfigyelhetőség](https://kasszajs.hu/docs/kiegeszitok/megfigyelhetoseg)
1177
+
1178
+ </details>
1179
+
1180
+ ## Tömeges és ismétlődő számlázás
1181
+
1182
+ Havidíj, tagdíj, bérleti díj: a `kassza/batch` idempotens kulcsokkal, sebességkorláttal és próbafuttatással állítja ki a bizonylatokat, és a Számlázz.hu korlátjánál megáll.
1183
+
1184
+ <details>
1185
+ <summary><b>Havi számlázás</b> · <code>runBatch()</code>, <code>billingPeriodAt()</code></summary>
1186
+
1187
+ ```ts
1188
+ import { billingPeriodAt, runBatch } from 'kassza/batch'
1189
+
1190
+ const idoszak = billingPeriodAt({ interval: 'month', anchor: '2026-01-31' }, new Date())
1191
+ const eredmeny = await runBatch(kassza, {
1192
+ items: elofizetesek.map((elofizetes) => ({
1193
+ key: `SUB-${elofizetes.id}-${idoszak.start}`,
1194
+ document: () => ({ kind: 'invoice', input: szamlaElofizetesbol(elofizetes, idoszak) }),
1195
+ })),
1196
+ dryRun: false,
1197
+ })
1198
+ ```
1199
+
1200
+ Egy megszakadt futás újraindítva nem állít ki duplát. A hónap végi kezdőnapot csúszás nélkül kezeli.
1201
+
1202
+ 📖 **Dokumentáció:** [Tömeges és ismétlődő számlázás](https://kasszajs.hu/docs/kiegeszitok/tomeges-szamlazas)
1203
+
1204
+ </details>
1205
+
1206
+ ## Node.js és Express
1207
+
1208
+ A webhook-kezelők Web-szabványos `(Request) => Response` függvények. Express, NestJS és Fastify alá a `kassza/node` köti be őket, a nyers törzs (aláírás!) megőrzésével.
1209
+
1210
+ <details>
1211
+ <summary><b>Express</b> · <code>toNodeHandler()</code></summary>
1212
+
1213
+ ```ts
1214
+ import { toNodeHandler } from 'kassza/node'
1215
+ import { stripeWebhook } from 'kassza/payments/stripe'
1216
+
1217
+ app.post('/webhooks/stripe', toNodeHandler(stripeWebhook({ secret, onPayment })))
1218
+ app.use(express.json())
1219
+ ```
1220
+
1221
+ A webhook útvonal az `express.json()` elé kerüljön; ha egy middleware már feldolgozta a törzset, a kassza érthető hibát ad.
1222
+
1223
+ 📖 **Dokumentáció:** [Node.js, Express, NestJS](https://kasszajs.hu/docs/kiegeszitok/node-express)
1224
+
1225
+ </details>
1226
+
1105
1227
  ## Parancssor és MCP szerver
1106
1228
 
1107
1229
  <details>
@@ -1113,7 +1235,7 @@ npx kassza xml preview szamla.json
1113
1235
  npx kassza invoice get --order REND-1001
1114
1236
  ```
1115
1237
 
1116
- A `doctor` ellenőrzi a Node.js-t, az Agent kulcsot, a gép óráját és a munkamenetet. Az `xml preview` kiírja a küldendő XML-t az Agent kulcs nélkül, így supportjegyhez is csatolható.
1238
+ A `doctor` ellenőrzi a Node.js-t, az Agent kulcsot, a gép óráját és a munkamenetet; a `--capabilities` az e-számla engedélyt, a `--report` titokmentes Markdown jelentést ad hibajegyhez. Az `xml preview` kiírja a küldendő XML-t az Agent kulcs nélkül, így supportjegyhez is csatolható.
1117
1239
 
1118
1240
  📖 **Dokumentáció:** [Parancssor](https://kasszajs.hu/docs/kiegeszitok/parancssor)
1119
1241
 
@@ -1158,6 +1280,14 @@ A weboldal is AI-barát: az [llms.txt](https://kasszajs.hu/llms.txt) a dokument
1158
1280
  <a href="https://kasszajs.hu/docs/kiegeszitok/ai-asszisztensek"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-dark.svg"><img src="https://raw.githubusercontent.com/futozs/kassza/main/readme/assets/button-docs-light.svg" alt="Dokumentáció: AI asszisztensek" height="44"></picture></a>
1159
1281
  </p>
1160
1282
 
1283
+ ## Ki használja?
1284
+
1285
+ A kassza nem küld semmilyen használati adatot, ezért csak az derül ki, ki használja, aki maga jelzi. Ha éles vagy kísérleti projektben használod, [írd meg egy issue-ban](https://github.com/futozs/kassza/issues/new?template=used-by.yml), és felkerülsz ide.
1286
+
1287
+ | Projekt | Mire használja |
1288
+ | --- | --- |
1289
+ | _Légy te az első._ | |
1290
+
1161
1291
  ---
1162
1292
 
1163
1293
  <p align="center">
package/agents/README.md CHANGED
@@ -14,6 +14,11 @@ It also links delegated (principal) accounts through the Agent (`connectPrincipa
14
14
  - `kassza/data-link`: a receiver for the Számlázz.hu financial data link (pushed invoices, bank transactions and receipts).
15
15
  - `kassza/testing`: a mock client and a fake Számla Agent `fetch` for integration tests.
16
16
  - `kassza/mcp` and the `kassza` CLI (`npx kassza doctor`, `npx kassza mcp`).
17
+ - `kassza/stores`: shared stores (Redis, Upstash, Cloudflare KV, Durable Object) for the session, the attempt ledger, the `createOnce` lock and webhook deduplication.
18
+ - `kassza/journal`: a document journal for the NAV daily summary, with pending entries and reconciliation.
19
+ - `kassza/batch`: rate-limited, restartable bulk and recurring invoicing, plus billing periods.
20
+ - `kassza/node`: Express, NestJS and `node:http` adapter for the webhook handlers.
21
+ - `kassza/observe`: structured logs, tracing and Prometheus metrics, with no telemetry.
17
22
 
18
23
  It runs on Node 22+, Bun, Deno, Cloudflare Workers and Vercel Edge.
19
24
 
@@ -23,7 +28,7 @@ It runs on Node 22+, Bun, Deno, Cloudflare Workers and Vercel Edge.
23
28
  2. [api.md](./api.md): every method, input and output.
24
29
  3. [recipes.md](./recipes.md): webhooks, exactly-once invoicing, receipts, IPN, PDF storage, serverless, tests, payment providers, NAV reports, delegated invoicing, data link.
25
30
 
26
- A ready-made Claude Code skill is in [skills/kassza/SKILL.md](./skills/kassza/SKILL.md). Copy the `skills/kassza` folder into `.claude/skills/` in the user's project.
31
+ A ready-made Claude Code skill is in [skills/kassza/SKILL.md](./skills/kassza/SKILL.md). To move a codebase off `szamlazz.js`, use [skills/migrate-from-szamlazz-js/SKILL.md](./skills/migrate-from-szamlazz-js/SKILL.md). Copy the `skills/kassza` folder into `.claude/skills/` in the user's project.
27
32
 
28
33
  ## The five things to get right
29
34
 
package/agents/api.md CHANGED
@@ -11,15 +11,18 @@ const kassza = createKassza({
11
11
  agentKey?: string,
12
12
  username?: string, password?: string,
13
13
  defaults?: { invoice?: InvoiceDefaults, receipt?: ReceiptDefaults },
14
- cookieStore?: CookieStore | false,
15
- attemptLedger?: CookieStore,
14
+ cookieStore?: KeyValueStore | false,
15
+ attemptLedger?: KeyValueStore,
16
+ attemptLedgerMode?: 'fail-open' | 'fail-closed',
17
+ createOnceLock?: KeyValueStore,
18
+ taxpayerCache?: { store: KeyValueStore, ttlSeconds?, invalidTtlSeconds? },
16
19
  timeoutMs?: number,
17
20
  maxAttempts?: number,
18
21
  retryDelayMs?: number,
19
22
  maintenanceCooldownMs?: number,
20
23
  fetch?: typeof fetch,
21
24
  endpoint?: string,
22
- hooks?: { onRequest?, onResponse?, onError?, onDocument? },
25
+ hooks?: { onRequest?, onResponse?, onError?, onComplete?, onDocument?, onDocumentError?, onWarning? },
23
26
  })
24
27
  ```
25
28
 
@@ -29,9 +32,15 @@ const kassza = createKassza({
29
32
  - `timeoutMs` defaults to 60000.
30
33
  - `maxAttempts` defaults to 3 and is capped at 5. It applies only to operations that are safe to retry.
31
34
  - `hooks` receive events for logging. They never receive the XML payload or the key.
32
- - `attemptLedger` is a shared store with the `CookieStore` interface (for example `upstashRedisCookieStore(redis)` from `kassza/cookie-stores`). It counts failed attempts of identical requests across processes for a day. After 5 failures kassza throws an `attempt_limit` error without sending the request; after a human fixed the cause, call `kassza.resetAttempts(error)`.
35
+ - `KeyValueStore` (alias `CookieStore`) is `{ get, set, delete }` plus the optional atomic `setIfAbsent`, `increment`, `deleteIfEquals`. Adapters live in `kassza/stores` (see below).
36
+ - `attemptLedger` is a shared store. It counts failed attempts of identical requests across processes for a day, atomically when the store has `increment`. After 5 failures kassza throws an `attempt_limit` error without sending the request; after a human fixed the cause, call `kassza.resetAttempts(error)`. If the store fails, `attemptLedgerMode: 'fail-open'` (default) falls back to an in-process counter and emits `onWarning`; `'fail-closed'` throws `store_unavailable` without sending (create the adapter with `resilient: false` so read errors reach kassza).
37
+ - `createOnceLock` is a shared store with `setIfAbsent` (Redis, Upstash, Durable Object; not Cloudflare KV). It makes `createOnce` exactly-once across processes. Calls for the same order inside one process are always merged, without any option.
38
+ - `taxpayerCache` caches `taxpayer.query` results (valid ones for a day, invalid ones for 10 minutes; errors are never cached).
33
39
  - `maintenanceCooldownMs` defaults to 0. When set, a maintenance error (code 1) blocks all requests of the client for that long, and they fail at once with category `maintenance`.
34
- - `hooks.onDocument(event)` runs after every created or reversed document and every registered payment, with `{ kind: 'invoice' | 'receipt', action: 'created' | 'reversed' | 'payment', number, document }` (plus `reversedNumber` for reversals and `input` for created invoices). It is awaited, and an error thrown in it is logged, never rethrown.
40
+ - `hooks.onDocument(event)` runs after every created or reversed document and every registered payment, with `{ kind: 'invoice' | 'receipt', action: 'created' | 'reversed' | 'payment', number, document }` (plus `reversedNumber` for reversals and `input` for created invoices). It is awaited. `hooks.onDocumentError` decides what happens when it throws: `'warn'` (default, the call still succeeds), `'throw'` (a `DocumentHookError` carrying `event`, `document` and `number` of the document that WAS created; never re-create it) or a handler `(error, event) => void`.
41
+ - `hooks.onWarning(warning)` receives the errors kassza swallows on purpose: `{ kind: 'session' | 'ledger' | 'hook' | 'document' | 'lock' | 'dedupe' | 'cache', message, error, action?, operation? }`.
42
+ - `hooks.onRequest`, `onResponse`, `onError` and `onComplete` events carry `{ action, attempt, requestId }`; `onResponse` adds `status`, `durationMs`, `responseBytes`, `sessionReused`; `onComplete` adds `outcome: 'success' | 'error'`, `durationMs`, `error?`, `willRetry` and fires once per attempt.
43
+ - Retries of safe operations wait `retryDelayMs * 2^(attempt-1)` with ±20% jitter, and honour `Retry-After` (no retry when it exceeds 60 s).
35
44
 
36
45
  ## Invoices: `kassza.invoices`
37
46
 
@@ -42,6 +51,7 @@ const kassza = createKassza({
42
51
  | `preview(input: CreateInvoiceInput)` | `InvoicePreview` (PDF only, no document is created) | never |
43
52
  | `reverse(input: string \| ReverseInvoiceOptions)` | `ReversedInvoice` | never |
44
53
  | `registerPayment(input: RegisterPaymentInput)` | `RegisteredPayment` | only when `additive: false` |
54
+ | `registerPaymentOnce({ invoiceNumber, key, amount, method?, date?, description?, taxNumber? }, options?)` | `RegisteredPaymentOnce` (`{ invoiceNumber, key, marker, created, payment?, existing? }`) | never resends; looks the payment up by its marker |
45
55
  | `clearPayments(input: string \| { invoiceNumber })` | `RegisteredPayment` | yes |
46
56
  | `getPdf(ref: InvoiceReference)` | `InvoicePdf` | yes |
47
57
  | `get(ref: InvoiceReference, { includePdf? })` | `InvoiceDetails` | yes |
@@ -52,7 +62,7 @@ const kassza = createKassza({
52
62
 
53
63
  ### createOnce
54
64
 
55
- `invoices.createOnce(input, { lookupFirst?, matchOrderNumber?, recoveryDelayMs?, signal? })` makes invoicing exactly-once:
65
+ `invoices.createOnce(input, { lookupFirst?, matchOrderNumber?, recoveryDelayMs?, signal?, lock?, lockTtlSeconds?, lockWaitMs?, lockFailure? })` makes invoicing exactly-once:
56
66
 
57
67
  1. It looks the invoice up by external ID (and, with `matchOrderNumber`, by order number) and returns it if it exists.
58
68
  2. Otherwise it creates the invoice.
@@ -62,6 +72,8 @@ const kassza = createKassza({
62
72
  - `lookupFirst` and `matchOrderNumber` default to `true`.
63
73
  - It returns `InvoiceOnceResult`: `{ number, created, externalId, invoice?: CreatedInvoice, details?: InvoiceDetails }`. `created` is `false` when the invoice already existed.
64
74
  - If the outcome stays unknown, it rethrows the original error with `details.outcome: 'unknown'`. Do not create the invoice by hand then; call `createOnce` again later.
75
+ - Concurrent calls for the same order in one process share one execution; the others get the result with `created: false`. Across processes `lock` (default: the client's `createOnceLock`, `false` disables) is held for `lockTtlSeconds` (600); a waiter polls up to `lockWaitMs` (10000) and then throws `in_progress` (respond 5xx in a webhook so it is redelivered). `lockFailure: 'throw'` (default) throws `store_unavailable` when the lock store is down; `'proceed'` continues without a lock and warns.
76
+ - `registerPaymentOnce` writes `[kassza:{key}]` into the payment description and looks for it on the invoice before registering. If the marker cannot be read back after registering, it throws `unexpected_response` with `details.registered: 'true'`: the payment exists, do not call again.
65
77
 
66
78
  ### CreateInvoiceInput
67
79
 
@@ -187,7 +199,7 @@ Notes on the fields:
187
199
 
188
200
  ## Taxpayer: `kassza.taxpayer`
189
201
 
190
- `query(taxNumber: string): TaxpayerInfo` accepts `12345676`, `12345676-2-42` or `HU12345676`.
202
+ `query(taxNumber: string): TaxpayerInfo` accepts `12345676`, `12345676-2-42` or `HU12345676`. With `taxpayerCache` the result is cached by the 8-digit taxpayer ID.
191
203
 
192
204
  `TaxpayerInfo` is `{ valid, name?, shortName?, taxNumber?: { taxpayerId, vatCode?, countyCode?, formatted? }, address?: TaxpayerAddress, addresses, incorporation?, infoDate? }`, and `TaxpayerAddress.formatted` looks like `'1031 Budapest, Záhony utca 7.'`.
193
205
 
@@ -261,13 +273,34 @@ createFakeAgentFetch(options?: FakeAgentOptions): FakeAgent
261
273
  ### `kassza/money`
262
274
 
263
275
  - `calculateInvoiceItem(input, currency?)` and `calculateReceiptItem(input, currency?)` return `ItemAmounts`.
276
+ - `allocateRefund(items: { name, vat, grossAmount, unit?, identifier? }[], refundGross, { decimals?, refundedBefore? })` splits a refund exactly over items (Sainte-Laguë, BigInt), path-independent: refunds split into parts give the same per-item totals as one refund.
264
277
  - `summarizeItems(items)` returns `{ netAmount, vatAmount, grossAmount, byVat }`.
265
278
  - `roundMoney(value, decimals)`, `isVatRate(value)`, `NUMERIC_VAT_RATES`, `SPECIAL_VAT_CODES`.
266
279
 
267
- ### `kassza/storage` and `kassza/cookie-stores`
280
+ ### `kassza/storage`
268
281
 
269
282
  - `storePdf(storage, key, pdf)` and `invoicePdfKey({ number })` save PDFs through adapters: `s3FetchStorage`, `s3Storage`, `r2BindingStorage`, `vercelBlobStorage`, `uploadthingStorage`, `supabaseStorage`, `memoryStorage`, and `fsStorage` from `kassza/storage/fs`.
270
- - Session and attempt-ledger stores: `upstashRedisCookieStore`, `ioredisCookieStore`, `nodeRedisCookieStore`, `cloudflareKvCookieStore`, `customCookieStore({ get, set, delete })`, `memoryCookieStore`.
283
+
284
+ ### `kassza/stores` (also `kassza/cookie-stores`)
285
+
286
+ - `memoryStore()`, `upstashRedisStore(redis)`, `ioredisStore(redis)`, `nodeRedisStore(client)`, `cloudflareKvStore(namespace)`, `customStore(store)`, `resilientStore(store)`; the old `*CookieStore` names are aliases.
287
+ - Redis adapters get the atomic operations through Lua (`eval`) when the client has it. Cloudflare KV has none: use `durableObjectStore(stub)` with `handleDurableObjectStoreRequest(this.ctx.storage, request)` inside your Durable Object class for locks and the journal on Workers.
288
+ - Adapters are resilient by default: `get`/`set`/`delete` errors are reported to `onError` and swallowed, atomic operation errors are reported and rethrown. `resilient: false` rethrows everything.
289
+ - `diagnoseStore(store)` returns `{ ok, capabilities, suitableFor: { session, attemptLedger, createOnceLock, journal, webhookDedupe }, latencyMs, problems }` after a round-trip with a temporary key.
290
+
291
+ ### `kassza/journal`
292
+
293
+ ```ts
294
+ const journal = createJournal(kvJournal(store), { now? })
295
+ createKassza({ hooks: { onDocument: (event) => journal.record(event), onDocumentError: 'throw' } })
296
+ ```
297
+
298
+ - `kvJournal(store, { prefix?, retentionDays? (400), reservationTtlDays? (30) })` needs a store with `increment`; `memoryJournal()` for tests; or implement `JournalStorage` for SQL.
299
+ - `record(event)` stores created and reversed documents (payments are ignored); idempotent by document number.
300
+ - `trackReceipt(orderNumber, run)` / `trackInvoice(orderNumber, run)` reserve before `run` and complete after it. Definite `SzamlazzError`s release the reservation; uncertain errors and crashes leave it pending. A failed journal write after success throws `JournalWriteError` with `result`.
301
+ - `pending(range?)` lists reservations (last 30 days by default); `settle(kassza, { range?, releaseNotFound? })` looks them up by order number and records what it finds.
302
+ - `entries({ from, to, kind? })`, `receipts({ from, to, includeTest? })` (feed into `navDailyReports`), at most 366 days per call.
303
+ - `reconcile(archive, { from?, to?, dryRun? })` compares with the data link receipt archive: `{ range, added, missingFromArchive, matched }`.
271
304
 
272
305
  ### `kassza/payments`
273
306
 
@@ -283,7 +316,8 @@ Webhook handlers are `(request: Request) => Promise<Response>`, usable directly
283
316
 
284
317
  - Common options: `onPayment(payment)`, `onError?(error)`, `method?` (the payment method written on the document), `maxBodyBytes?`, `fetch?`; `sandbox?` where the provider has one.
285
318
  - Responses: `400` for a bad signature or payload (never reaches `onPayment`), `200` on success (`204` for Revolut, a signed JSON for SimplePay), `500` when `onPayment` throws, so the provider redelivers.
286
- - `PaymentEvent` is `{ provider, kind: 'paid' | 'refunded' | 'partially-refunded' | 'failed' | 'other', id, eventId?, eventType?, orderRef?, amount?: { value, currency }, refundedAmount?, paidAt?, method, customer?, items?, raw }`. Amounts are in major units (12700 HUF, not minor units).
319
+ - `PaymentEvent` is `{ provider, kind: 'paid' | 'refunded' | 'partially-refunded' | 'failed' | 'other', id, eventId?, eventType?, orderRef?, amount?: { value, currency }, refundedAmount?, refunds?: { id, amount, refundedBefore?, createdAt? }[], paidAt?, method, customer?, items?, raw }`. Amounts are in major units (12700 HUF, not minor units). `refunds` lists the individual refunds the provider reported (Barion and SimplePay: all; Revolut and PayPal: the current one; Stripe: the delta of `charge.refunded` keyed by the event ID).
320
+ - `dedupe?: KeyValueStore` and `dedupeTtlSeconds?` (7 days): an event that was already processed successfully is acknowledged without calling `onPayment` again. It is marked only after `onPayment` succeeds.
287
321
 
288
322
  ```ts
289
323
  const result = await kassza.issueForPayment(payment, {
@@ -297,18 +331,36 @@ const result = await kassza.issueForPayment(payment, {
297
331
  orderNumber?: string,
298
332
  exchangeRate?, exchangeBank?,
299
333
  allowAmountMismatch?: boolean,
334
+ partialRefund?: 'auto' | 'proportional' | 'skip',
335
+ refundItems?: ({ refund, invoiceNumber, items }) => PaymentDocumentItem[] | undefined,
300
336
  receipt?: Partial<CreateReceiptInput>,
301
337
  invoice?: Partial<CreateInvoiceInput>,
302
338
  })
303
339
  ```
304
340
 
305
- - `paid`: chooses receipt or invoice with `chooseDocument` (override with `document`) and issues it with `createOnce`. `refunded`: reverses that document exactly once. `partially-refunded`, `failed` and `other`: skipped, because a partial refund needs a corrective document decided by a human.
341
+ - `paid`: chooses receipt or invoice with `chooseDocument` (override with `document`) and issues it with `createOnce`. `refunded`: reverses that document exactly once. `failed` and `other`: skipped.
342
+ - `partially-refunded`: for an invoice, one corrective invoice per refund (external ID `{orderNumber}/R-{refund}`), with the refund allocated exactly over the original items (`allocateRefund`, house-monotone, never more than an item's original amount across refunds). If the invoice has several VAT rates and no `refundItems` is given, it returns `{ kind: 'refund-proposal', ... }` instead (or allocate proportionally with `partialRefund: 'proportional'`). For a receipt it always returns a `refund-proposal`. Without `payment.refunds` it is skipped with a reason.
306
343
  - The order number defaults to `{PROVIDER}-{payment id}`, for example `STRIPE-pi_123`, so the payment and its refund share it.
307
344
  - Items come from `items`, then the provider's line items, then one item with the paid amount (`fallbackItemName`). VAT comes from `vatFor`, the provider's per-item rate, then `vat`. Without any, it throws `validation`: kassza never guesses VAT.
308
345
  - The document total must equal the paid amount, unless `allowAmountMismatch: true`.
309
- - It returns `{ kind: 'receipt', number, created, receipt, decision, orderNumber }`, `{ kind: 'invoice', number, created, invoice, decision, orderNumber }`, `{ kind: 'reversal', document, reversedNumber, number?, created, orderNumber }` or `{ kind: 'skipped', reason, orderNumber }`.
346
+ - It returns `{ kind: 'receipt', number, created, receipt, decision, orderNumber }`, `{ kind: 'invoice', number, created, invoice, decision, orderNumber }`, `{ kind: 'reversal', document, reversedNumber, number?, created, orderNumber }`, `{ kind: 'correction', correctedNumber, created, corrections: { refundId, externalId, number, created, grossTotal, invoice }[], orderNumber }`, `{ kind: 'refund-proposal', document, documentNumber, refunds: { refundId, grossTotal, items }[], reason, orderNumber }` or `{ kind: 'skipped', reason, orderNumber }`.
310
347
  - `issueForPayment(api, payment, options)` from `kassza/payments` does the same with any object that has `invoices` and `receipts` (`createOnce`, `find`, `reverse`), for example a delegated client.
311
348
 
349
+ ### `kassza/batch`
350
+
351
+ - `runBatch(kassza, { items: { key, document: () => ({ kind: 'invoice' | 'receipt', input }) }[], concurrency? (1, max 4), ratePerMinute? (30), dryRun?, journal?, signal?, onProgress?, recoveryDelayMs? })` issues each item with `createOnce`, using `key` as the order number, so a rerun never duplicates. It stops (marking the rest `skipped`) on `rate_limit`, `auth`, `account`, `configuration`, `maintenance`, `attempt_limit` or `store_unavailable`; other item errors land in `failed`. `dryRun` previews invoices and computes receipts locally. Returns `{ dryRun, items, created, existing, previewed, failed, skipped, stopped?, grossTotal }`.
352
+ - `billingPeriod({ interval: 'week' | 'month' | 'quarter' | 'year', anchor, paymentDueInDays? }, index)`, `billingPeriodAt(options, date)`, `billingPeriodsBetween(options, from, to)`, `todayPeriod(options)` return `{ index, interval, start, end, dueDate?, key }` in Budapest dates; month ends never drift (Jan 31 → Feb 28/29 → Mar 31). The fulfilment date is the caller's legal decision.
353
+
354
+ ### `kassza/node`
355
+
356
+ - `toNodeHandler(webHandler, { trustProxy?, onError? })` adapts any `(Request) => Response` handler to Express, NestJS and `node:http`. It streams the raw body (so signatures verify), uses `rawBody` (NestJS) or a Buffer `body` (`express.raw`), and throws `NodeBodyError` when `express.json()` already consumed the body. With Express `next`, errors go to `next`.
357
+ - `toWebRequest(nodeRequest)` and `writeWebResponse(response, nodeResponse)` for Fastify or custom servers.
358
+
359
+ ### `kassza/observe`
360
+
361
+ - `observe({ logger?, tracer?, metrics?, redact? (true), orderRefSalt?, logRequests? })` returns hooks: structured logs without secrets (order numbers as `hmac:` refs), one span per attempt (OpenTelemetry `Tracer` compatible), and metrics. `combineHooks(...hooks)` merges hook sets.
362
+ - `createMetricsRegistry(buckets?)` is a dependency-free registry with `renderPrometheus()`. Metrics: `kassza_requests_total{action,outcome}`, `kassza_request_duration_ms{action}`, `kassza_retries_total`, `kassza_maintenance_total`, `kassza_unexpected_response_total`, `kassza_store_errors_total{store}`, `kassza_warnings_total{kind}`, `kassza_documents_total{kind,action}`.
363
+
312
364
  ### `kassza/delegation`
313
365
 
314
366
  ```ts
@@ -12,7 +12,9 @@ Read this before writing any code that issues invoices or receipts. These rules
12
12
  6. **Use the test account while developing.** It allows at most 500 invoices per 10 minutes (error `167`, `rate_limit`). Never run tests against a production Agent key.
13
13
  7. **An Agent receipt is a computer-generated receipt (számítógéppel előállított nyugta).** It is not an online cash register receipt and not an e-receipt (e-nyugta). Issue it only for activities without the online cash register (OPG) obligation. Fixed-location retail (TEÁOR 47.1–47.7), restaurants and bars (56.1, 56.3, except mobile catering), accommodation (55.1–55.3), rental (77.1–77.2, 77.33), repair (95.1–95.2) and pharmacies need an online cash register or e-cash register instead. Typical valid uses: webshops, online tickets, downloadable products, food trucks and other mobile sales, services without a cash register obligation.
14
14
  8. **A receipt may replace an invoice only if all four conditions hold:** the buyer is not a taxable person or legal entity, the total is below 900 000 HUF, it is paid in full by fulfilment, and the buyer did not ask for an invoice. `chooseDocument()` applies these rules, and `issueForPayment()` uses it. If the buyer asks for an invoice later, use `receipts.convertToInvoice()`.
15
- 9. **Never submit NAV receipt reports for receipts issued in Számlázz.hu.** Számlázz.hu reports them itself after the NAV connection, so a second submission is double reporting. The `kassza/nav` client is read-only unless you pass `allowWrite: true`; submit only reports of paper receipt pads.
15
+ 9. **In serverless or multi-process setups, give `createOnce` a shared lock.** Two concurrent deliveries of the same webhook on two instances can both see "no invoice yet". Pass `createOnceLock: store` (Redis, Upstash or Durable Object from `kassza/stores`; Cloudflare KV cannot lock). Calls inside one process are merged automatically.
16
+ 10. **If your `onDocument` hook writes a legally relevant journal, set `onDocumentError: 'throw'`.** Otherwise a failed write is only a warning and the NAV daily summary silently misses a receipt. Use `kassza/journal` for this.
17
+ 11. **Never submit NAV receipt reports for receipts issued in Számlázz.hu.** Számlázz.hu reports them itself after the NAV connection, so a second submission is double reporting. The `kassza/nav` client is read-only unless you pass `allowWrite: true`; submit only reports of paper receipt pads.
16
18
 
17
19
  ## Behaviour worth knowing
18
20
 
@@ -42,7 +44,14 @@ Read this before writing any code that issues invoices or receipts. These rules
42
44
  | Error `250` | A delegated account has not been taken over by its owner, or the delegation is not accepted | Wait for the principal; check with `probeDelegation()` rarely, never in a loop |
43
45
  | Error `167` (`rate_limit`) | Too many documents in the test account in a short time | Wait a few minutes; never retry automatically |
44
46
  | `attempt_limit` error | The same request already failed 5 times (counted across processes with `attemptLedger`) | A human fixes the cause, then `kassza.resetAttempts(error)` |
47
+ | `attemptLedger` limits | The ledger is conservative: network and timeout failures count too, and the check (read) and the count (increment) are separate store calls, so many processes sending the *same* request at the very same moment can each pass the check | Pair it with `createOnceLock` for documents, so only one process sends at a time |
45
48
  | `maintenanceCooldownMs` | After error `1`, kassza sends nothing for the cooldown and throws `maintenance` at once | Switch to a fallback, for example a paper receipt pad |
49
+ | `in_progress` error | Another process holds the `createOnce` lock for this order | Do not issue by hand; call `createOnce` again later (5xx in a webhook) |
50
+ | `store_unavailable` error | The lock store (or a `fail-closed` attempt ledger) is down; nothing was sent | Restore the store, or choose `lockFailure: 'proceed'` / `attemptLedgerMode: 'fail-open'` deliberately |
51
+ | Partial refund on a mixed-VAT invoice | `issueForPayment` returns `refund-proposal`, issues nothing | Pass `refundItems` (what was returned) or `partialRefund: 'proportional'` |
52
+ | `registerPayment` twice | Two payments on the invoice (`additive` defaults to `true`) | Use `registerPaymentOnce({ key })` with a stable bank or payment ID |
53
+ | `szamlaLetoltesPld` (PDF copies) | Obsolete in the XSD; Számlázz.hu ignores it, so kassza does not offer it | Nothing to do |
54
+ | `express.json()` before a webhook route | The raw body is gone, signatures cannot be verified | Mount the route before `express.json()`, or use `express.raw()`; see `kassza/node` |
46
55
 
47
56
  ## Error categories
48
57
 
@@ -59,6 +68,8 @@ Read this before writing any code that issues invoices or receipts. These rules
59
68
  | `maintenance` | Számlázz.hu maintenance (code 1) | kassza retries lookups automatically |
60
69
  | `rate_limit` | Too many documents in the test account (code 167) | No, wait a few minutes |
61
70
  | `attempt_limit` | The request already failed 5 times; kassza did not send it | No, a human must act, then `resetAttempts` |
71
+ | `in_progress` | Another process is issuing the same document (`createOnce` lock) | Later, by calling `createOnce` again |
72
+ | `store_unavailable` | A required store (lock, fail-closed ledger) is down; nothing was sent | After the store is back |
62
73
  | `network` / `timeout` | Transport failure; the outcome is unknown for writes | Look up by `orderNumber` before trying again |
63
74
  | `configuration` | Missing key, bad options | No |
64
75
  | `unexpected_response` | Számlázz.hu answered in an unknown format | Report it |