kassza 0.12.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.
- package/README.en.md +48 -0
- package/README.md +427 -32
- package/agents/README.md +26 -6
- package/agents/api.md +218 -3
- package/agents/pitfalls.md +27 -3
- package/agents/recipes.md +280 -51
- package/agents/skills/kassza/SKILL.md +14 -8
- package/agents/skills/migrate-from-szamlazz-js/SKILL.md +39 -0
- package/dist/batch/index.cjs +346 -0
- package/dist/batch/index.d.cts +91 -0
- package/dist/batch/index.d.ts +91 -0
- package/dist/batch/index.js +339 -0
- package/dist/cli.cjs +643 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +643 -0
- package/dist/client-CLFvJCVW.cjs +1452 -0
- package/dist/{client-CL3fuH6A.js → client-Cvf-hTz_.js} +306 -515
- package/dist/client-DeWWQ-RZ.d.ts +341 -0
- package/dist/client-SVZu25MB.d.cts +341 -0
- package/dist/context-CFNKzStE.d.cts +449 -0
- package/dist/context-kb33TgzE.d.ts +449 -0
- package/dist/cookie-stores/index.cjs +21 -150
- package/dist/cookie-stores/index.d.cts +4 -63
- package/dist/cookie-stores/index.d.ts +4 -63
- package/dist/cookie-stores/index.js +3 -144
- package/dist/create-CflXrPip.js +204 -0
- package/dist/create-CvUvLUaT.cjs +257 -0
- package/dist/{create-once-D0pwuenl.cjs → create-once-9yxR_Er_.cjs} +291 -33
- package/dist/create-once-Bh6fOWkW.d.cts +20 -0
- package/dist/{create-once-CqdYmQD8.js → create-once-Ca2gMk1h.js} +250 -34
- package/dist/create-once-VKp0tVow.d.ts +20 -0
- package/dist/{crypto-B8lERvR1.cjs → crypto-Dpn4FHQ6.cjs} +9 -0
- package/dist/{crypto-DK_osBVy.js → crypto-usDg8L2K.js} +4 -1
- package/dist/data-link/index.cjs +232 -0
- package/dist/data-link/index.d.cts +29 -0
- package/dist/data-link/index.d.ts +29 -0
- package/dist/data-link/index.js +224 -0
- package/dist/delegation/index.cjs +35 -34
- package/dist/delegation/index.d.cts +2 -1
- package/dist/delegation/index.d.ts +2 -1
- package/dist/delegation/index.js +4 -3
- package/dist/{errors-DLrb0g64.js → errors-DtN4g5Z9.js} +9 -1
- package/dist/{errors-DxeuTslP.cjs → errors-EE96_6D_.cjs} +20 -0
- package/dist/get-BdMTseh4.cjs +271 -0
- package/dist/get-Byc9T5WO.d.cts +151 -0
- package/dist/get-Byc9T5WO.d.ts +151 -0
- package/dist/get-CZEShCcj.js +260 -0
- package/dist/index-BhUPY_EH.d.cts +109 -0
- package/dist/index-BqGgQ4t3.d.ts +109 -0
- package/dist/{index-CYAGCdKJ.d.cts → index-C-7yWV7L.d.cts} +24 -1
- package/dist/{index-CYAGCdKJ.d.ts → index-C-7yWV7L.d.ts} +24 -1
- package/dist/index.cjs +19 -5
- package/dist/index.d.cts +11 -19
- package/dist/index.d.ts +11 -19
- package/dist/index.js +6 -6
- package/dist/ipn/index.cjs +26 -5
- package/dist/ipn/index.d.cts +12 -1
- package/dist/ipn/index.d.ts +12 -1
- package/dist/ipn/index.js +26 -6
- package/dist/issue-Bin4tKD_.cjs +988 -0
- package/dist/issue-DMA1Nbr1.js +905 -0
- package/dist/journal/index.cjs +454 -0
- package/dist/journal/index.d.cts +18 -0
- package/dist/journal/index.d.ts +18 -0
- package/dist/journal/index.js +445 -0
- package/dist/journal-DvI81Kt7.d.cts +114 -0
- package/dist/journal-Y9qrIB8G.d.ts +114 -0
- package/dist/mcp/index.cjs +10 -0
- package/dist/mcp/index.d.cts +71 -0
- package/dist/mcp/index.d.ts +71 -0
- package/dist/mcp/index.js +2 -0
- package/dist/money/index.cjs +2 -1
- package/dist/money/index.d.cts +2 -2
- package/dist/money/index.d.ts +2 -2
- package/dist/money/index.js +2 -2
- package/dist/{money-DAA7OwiO.js → money-Cw457SD6.js} +120 -12
- package/dist/{money-DrhGkWHW.cjs → money-K9j0wPit.cjs} +124 -10
- package/dist/nav/index.cjs +1046 -0
- package/dist/nav/index.d.cts +247 -0
- package/dist/nav/index.d.ts +247 -0
- package/dist/nav/index.js +1012 -0
- package/dist/nav-BNbc8U0R.cjs +203 -0
- package/dist/nav-BcCerwWS.js +156 -0
- package/dist/node/index.cjs +124 -0
- package/dist/node/index.d.cts +35 -0
- package/dist/node/index.d.ts +35 -0
- package/dist/node/index.js +120 -0
- package/dist/observe/index.cjs +311 -0
- package/dist/observe/index.d.cts +53 -0
- package/dist/observe/index.d.ts +53 -0
- package/dist/observe/index.js +303 -0
- package/dist/once-BN_3B5OF.cjs +103 -0
- package/dist/once-CqqZxsXG.js +74 -0
- package/dist/parse-Cr7yQ3_z.js +291 -0
- package/dist/parse-DOA2XQ3e.cjs +380 -0
- package/dist/payments/barion.cjs +26 -2
- package/dist/payments/barion.d.cts +2 -2
- package/dist/payments/barion.d.ts +2 -2
- package/dist/payments/barion.js +26 -2
- package/dist/payments/index.cjs +7 -2
- package/dist/payments/index.d.cts +4 -4
- package/dist/payments/index.d.ts +4 -4
- package/dist/payments/index.js +3 -3
- package/dist/payments/paypal.cjs +16 -2
- package/dist/payments/paypal.d.cts +2 -2
- package/dist/payments/paypal.d.ts +2 -2
- package/dist/payments/paypal.js +16 -2
- package/dist/payments/revolut.cjs +17 -3
- package/dist/payments/revolut.d.cts +2 -2
- package/dist/payments/revolut.d.ts +2 -2
- package/dist/payments/revolut.js +17 -3
- package/dist/payments/simplepay.cjs +37 -5
- package/dist/payments/simplepay.d.cts +2 -2
- package/dist/payments/simplepay.d.ts +2 -2
- package/dist/payments/simplepay.js +37 -5
- package/dist/payments/stripe.cjs +20 -3
- package/dist/payments/stripe.d.cts +2 -2
- package/dist/payments/stripe.d.ts +2 -2
- package/dist/payments/stripe.js +20 -3
- package/dist/push-BXj0Sf0Z.d.ts +52 -0
- package/dist/push-D4nNbCNV.d.cts +52 -0
- package/dist/reference-B5TATqb5.js +124 -0
- package/dist/reference-BIdYMK0t.cjs +183 -0
- package/dist/reports/index.cjs +11 -164
- package/dist/reports/index.d.cts +1 -1
- package/dist/reports/index.d.ts +1 -1
- package/dist/reports/index.js +1 -154
- package/dist/request-body-CH6ZGOrq.cjs +34 -0
- package/dist/request-body-uZHMRlQr.js +29 -0
- package/dist/serialize-B2eFh3D-.js +219 -0
- package/dist/serialize-CquIsxA-.cjs +290 -0
- package/dist/server-BrJUbXs7.js +1505 -0
- package/dist/server-Dw9j8Mnd.cjs +1558 -0
- package/dist/session-Ckl5eJhV.d.ts +7 -0
- package/dist/{session-CKOuE6Jc.cjs → session-DeM5vjvC.cjs} +69 -13
- package/dist/{session-rMKaJcmq.js → session-SaEEXu2N.js} +58 -14
- package/dist/session-YbChChx7.d.cts +7 -0
- package/dist/storage/fs.cjs +1 -1
- package/dist/storage/fs.js +1 -1
- package/dist/storage/index.cjs +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/store-C1rHd9DB.d.cts +19 -0
- package/dist/store-C1rHd9DB.d.ts +19 -0
- package/dist/stores/index.cjs +24 -0
- package/dist/stores/index.d.cts +4 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +3 -0
- package/dist/stores-BNrS0dWl.cjs +506 -0
- package/dist/stores-DhjGBrl_.js +441 -0
- package/dist/testing/index.cjs +1500 -15
- package/dist/testing/index.d.cts +198 -3
- package/dist/testing/index.d.ts +198 -3
- package/dist/testing/index.js +1498 -15
- package/dist/{types-COf1LAD-.d.ts → types-B1A8mUIE.d.cts} +14 -2
- package/dist/{types-COf1LAD-.d.cts → types-ByAEvGsZ.d.ts} +14 -2
- package/dist/{types-k--zHf4l.d.ts → types-D1zDOH_F.d.ts} +1 -1
- package/dist/{types-DH8QoNZK.d.cts → types-lnKkQdot.d.cts} +1 -1
- package/dist/warnings-B3DXnveC.d.cts +31 -0
- package/dist/warnings-B3DXnveC.d.ts +31 -0
- package/dist/warnings-DeIw5RFj.cjs +20 -0
- package/dist/warnings-DwTl5J-2.js +15 -0
- package/dist/{webhook-D4lvtex1.js → webhook-CQVShAfI.js} +47 -26
- package/dist/{webhook-C76bV-4R.cjs → webhook-Ckj-_hPD.cjs} +70 -25
- package/dist/webhook-CmdhGiH5.d.cts +11 -0
- package/dist/webhook-D6lEejob.d.ts +11 -0
- package/llms.txt +6 -5
- package/package.json +134 -4
- package/dist/client-CickRRFr.d.ts +0 -838
- package/dist/client-CsmGIFRS.cjs +0 -1637
- package/dist/client-DvWZoX_E.d.cts +0 -838
- package/dist/issue-BsyYdrIG.cjs +0 -1531
- package/dist/issue-C6Fcjvln.js +0 -1274
- package/dist/session-Dm_45x8K.d.cts +0 -11
- package/dist/session-Dm_45x8K.d.ts +0 -11
- package/dist/webhook-CfaDgcL0.d.cts +0 -6
- package/dist/webhook-q2OzNwd7.d.ts +0 -6
package/agents/recipes.md
CHANGED
|
@@ -17,50 +17,37 @@ export const kassza = createKassza({
|
|
|
17
17
|
|
|
18
18
|
The client reads `SZAMLAZZ_AGENT_KEY` from the environment. Create it once per process, not once per request, so the session cookie is reused.
|
|
19
19
|
|
|
20
|
-
## 2. Paid order → invoice,
|
|
20
|
+
## 2. Paid order → invoice, exactly once (any webhook or job)
|
|
21
21
|
|
|
22
22
|
```ts
|
|
23
|
-
import {
|
|
24
|
-
import { kassza } from './kassza'
|
|
25
|
-
|
|
26
|
-
export async function invoiceOrder(order: Order) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
quantity: line.quantity,
|
|
48
|
-
grossUnitPrice: line.unitPriceHuf,
|
|
49
|
-
vat: 27,
|
|
50
|
-
})),
|
|
51
|
-
})
|
|
52
|
-
return invoice.number
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (isSzamlazzError(error) && ['network', 'timeout', 'partial_success', 'duplicate'].includes(error.category)) {
|
|
55
|
-
const created = await kassza.invoices.find({ orderNumber })
|
|
56
|
-
if (created) return created.header.number
|
|
57
|
-
}
|
|
58
|
-
throw error
|
|
59
|
-
}
|
|
23
|
+
import type { Kassza } from 'kassza'
|
|
24
|
+
import { kassza as defaultKassza } from './kassza'
|
|
25
|
+
|
|
26
|
+
export async function invoiceOrder(order: Order, kassza: Kassza = defaultKassza) {
|
|
27
|
+
const { number } = await kassza.invoices.createOnce({
|
|
28
|
+
orderNumber: `ORDER-${order.id}`,
|
|
29
|
+
paid: true,
|
|
30
|
+
paymentMethod: 'bankkártya',
|
|
31
|
+
buyer: {
|
|
32
|
+
name: order.billingName,
|
|
33
|
+
zip: order.zip,
|
|
34
|
+
city: order.city,
|
|
35
|
+
address: order.street,
|
|
36
|
+
email: order.email,
|
|
37
|
+
taxNumber: order.taxNumber,
|
|
38
|
+
},
|
|
39
|
+
items: order.lines.map((line) => ({
|
|
40
|
+
name: line.title,
|
|
41
|
+
quantity: line.quantity,
|
|
42
|
+
grossUnitPrice: line.unitPriceHuf,
|
|
43
|
+
vat: 27,
|
|
44
|
+
})),
|
|
45
|
+
})
|
|
46
|
+
return number
|
|
60
47
|
}
|
|
61
48
|
```
|
|
62
49
|
|
|
63
|
-
|
|
50
|
+
`createOnce` looks the invoice up first, so a redelivered webhook gets the existing number. After a network error, a timeout or error 56 it looks the invoice up again before giving up, and it never sends the invoice twice. For Stripe, SimplePay, Barion, Revolut and PayPal, recipe 11 does all of this with a ready-made handler.
|
|
64
51
|
|
|
65
52
|
## 3. Event registration: proforma → payment → invoice
|
|
66
53
|
|
|
@@ -100,18 +87,25 @@ export async function POST(request: Request) {
|
|
|
100
87
|
|
|
101
88
|
Set the webhook URL in Számlázz.hu under Fiók beállítások / Számlázás alapadatok. `markOrderPaid` must be idempotent, because the same notification can arrive more than once.
|
|
102
89
|
|
|
103
|
-
## 5.
|
|
90
|
+
## 5. Point-of-sale receipt (food truck, webshop, services)
|
|
104
91
|
|
|
105
92
|
```ts
|
|
106
|
-
const receipt = await kassza.receipts.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
93
|
+
const { receipt } = await kassza.receipts.createOnce(
|
|
94
|
+
{
|
|
95
|
+
orderNumber: `POS-${sale.id}`,
|
|
96
|
+
paymentMethod: sale.card ? 'bankkártya' : 'készpénz',
|
|
97
|
+
items: sale.lines.map((line) => ({ name: line.name, quantity: line.qty, grossUnitPrice: line.price, vat: line.vat })),
|
|
98
|
+
},
|
|
99
|
+
{ lookupFirst: false },
|
|
100
|
+
)
|
|
111
101
|
|
|
112
102
|
if (sale.email) await kassza.receipts.send({ receiptNumber: receipt.number, emails: sale.email })
|
|
113
103
|
```
|
|
114
104
|
|
|
105
|
+
- Agent receipts are only for activities without the online cash register obligation (rule 7 in [pitfalls.md](./pitfalls.md)): a food truck yes, a fixed shop or café no.
|
|
106
|
+
- The order number is also sent as `callId`, so a second tap on the button returns the same receipt (error 338 is handled for you). `lookupFirst: false` skips the lookup before each new sale.
|
|
107
|
+
- If Számlázz.hu is down, sell on a paper receipt pad and report it with `paperReceiptReport` (recipe 13).
|
|
108
|
+
|
|
115
109
|
## 6. Save the PDF to S3 or Cloudflare R2 (no AWS SDK needed)
|
|
116
110
|
|
|
117
111
|
```ts
|
|
@@ -142,19 +136,21 @@ Other adapters with the same interface:
|
|
|
142
136
|
- `fsStorage` from `kassza/storage/fs`
|
|
143
137
|
- `memoryStorage`
|
|
144
138
|
|
|
145
|
-
## 7. Serverless and edge: share the session
|
|
139
|
+
## 7. Serverless and edge: share the session, the ledger and the lock
|
|
146
140
|
|
|
147
141
|
```ts
|
|
148
142
|
import { Redis } from '@upstash/redis'
|
|
149
143
|
import { createKassza } from 'kassza'
|
|
150
|
-
import {
|
|
144
|
+
import { upstashRedisStore } from 'kassza/stores'
|
|
151
145
|
|
|
152
|
-
const
|
|
146
|
+
const store = upstashRedisStore(Redis.fromEnv())
|
|
147
|
+
const kassza = createKassza({ cookieStore: store, attemptLedger: store, createOnceLock: store })
|
|
153
148
|
```
|
|
154
149
|
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
150
|
+
- `createOnceLock` prevents a duplicate invoice when two instances process the same order at the same time.
|
|
151
|
+
- On Cloudflare Workers, `cloudflareKvStore(env.KASSZA_KV)` is fine for the session, but locks and the journal need `durableObjectStore(stub)` (KV has no atomic write).
|
|
152
|
+
- Other stores: `ioredisStore`, `nodeRedisStore`, `customStore({ get, set, delete })`. `diagnoseStore(store)` tells what a store is suitable for.
|
|
153
|
+
- If the session store fails, kassza continues without a session and reports it to `hooks.onWarning`.
|
|
158
154
|
|
|
159
155
|
## 8. Unit tests without calling Számlázz.hu
|
|
160
156
|
|
|
@@ -215,3 +211,236 @@ await kassza.invoices.create({
|
|
|
215
211
|
```
|
|
216
212
|
|
|
217
213
|
The exchange rate comes from MNB automatically when `exchangeRate` is omitted. Choose the VAT code together with an accountant.
|
|
214
|
+
|
|
215
|
+
## 11. Payment provider webhook → receipt or invoice (Stripe, SimplePay, Barion, Revolut, PayPal)
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// app/api/stripe/webhook/route.ts
|
|
219
|
+
import { isSzamlazzError } from 'kassza'
|
|
220
|
+
import { stripeWebhook } from 'kassza/payments/stripe'
|
|
221
|
+
import { kassza } from '@/lib/kassza'
|
|
222
|
+
|
|
223
|
+
export const POST = stripeWebhook({
|
|
224
|
+
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
225
|
+
apiKey: process.env.STRIPE_SECRET_KEY,
|
|
226
|
+
onPayment: async (payment) => {
|
|
227
|
+
try {
|
|
228
|
+
await kassza.issueForPayment(payment, { vat: 27 })
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (isSzamlazzError(error) && !error.retryable) {
|
|
231
|
+
await parkForHuman(payment.id, `${error.category}: ${error.message}`)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
throw error
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- The handler verifies the signature and answers 400, 200 or 500 itself. A thrown error means 500, and the provider redelivers, which is safe: `issueForPayment` looks the document up first.
|
|
241
|
+
- Catch non-retryable errors (a missing VAT rate, an incomplete billing address) and hand them to a human, otherwise the provider keeps redelivering a request that can never succeed. `parkForHuman` is your own function.
|
|
242
|
+
- The same pattern works with `simplePayWebhook`, `barionWebhook`, `revolutWebhook` and `payPalWebhook`; only the credentials differ (see [api.md](./api.md)).
|
|
243
|
+
- A full refund reverses the document. A partial refund of an invoice issues one corrective invoice per refund; with several VAT rates (and always for receipts) it returns a `refund-proposal` instead, unless you pass `refundItems` or `partialRefund: 'proportional'`.
|
|
244
|
+
- Add `dedupe: store` to acknowledge redelivered events without calling Számla Agent again.
|
|
245
|
+
- Under Express or NestJS, wrap the handler: `app.post(path, toNodeHandler(stripeWebhook({ ... })))` from `kassza/node`, mounted before `express.json()`.
|
|
246
|
+
- The receipt prefix and payment method come from the client's `defaults.receipt` (recipe 1). Pass `receipt: { ... }` or `invoice: { ... }` only to override fields; a receipt prefix must never be one already used on invoices (error 336).
|
|
247
|
+
|
|
248
|
+
## 12. Receipt or invoice, and an invoice after a receipt
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import { chooseDocument } from 'kassza'
|
|
252
|
+
|
|
253
|
+
const decision = chooseDocument({
|
|
254
|
+
grossTotal: order.total,
|
|
255
|
+
buyer: { taxNumber: order.taxNumber },
|
|
256
|
+
invoiceRequested: order.wantsInvoice,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (decision.type === 'receipt') {
|
|
260
|
+
await kassza.receipts.createOnce({ orderNumber: `ORDER-${order.id}`, paymentMethod: 'bankkártya', items })
|
|
261
|
+
} else {
|
|
262
|
+
await kassza.invoices.createOnce({ orderNumber: `ORDER-${order.id}`, paid: true, buyer, items })
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
When a buyer asks for an invoice after getting a receipt:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const converted = await kassza.receipts.convertToInvoice({
|
|
270
|
+
receiptNumber: 'NYGT-2026-118',
|
|
271
|
+
buyer: { name: 'Példa Kft.', zip: '1111', city: 'Budapest', address: 'Fő utca 1.', taxNumber: '12345676-2-42' },
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
converted.reversal?.number
|
|
275
|
+
converted.invoice.number
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
It reverses the receipt and invoices the same items, exactly once (external ID `CONV/{receiptNumber}`).
|
|
279
|
+
|
|
280
|
+
## 13. NAV daily receipt report: check it, do not double-report
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { createNavReceiptClient, reconcileNavReports } from 'kassza/nav'
|
|
284
|
+
import { navDailyReports } from 'kassza/reports'
|
|
285
|
+
|
|
286
|
+
const nav = createNavReceiptClient({
|
|
287
|
+
environment: 'production',
|
|
288
|
+
login: process.env.NAV_LOGIN!,
|
|
289
|
+
password: process.env.NAV_PASSWORD!,
|
|
290
|
+
signatureKey: process.env.NAV_SIGNATURE_KEY!,
|
|
291
|
+
taxNumber: process.env.NAV_TAX_NUMBER!,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const local = navDailyReports(await receiptsBetween('2026-09-01', '2026-09-30'))
|
|
295
|
+
const remote = await nav.listAllReports({ from: '2026-09-01', to: '2026-09-30' })
|
|
296
|
+
const { missing, mismatched } = reconcileNavReports(local, remote)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
- Számlázz.hu reports the receipts issued there after the NAV connection, so this client stays read-only (no `allowWrite`). Alert a human about `missing` and `mismatched` days.
|
|
300
|
+
- Get the receipts from `kassza/journal`: `journal.receipts({ from, to })` (see recipe 18). Without it you need your own query over stored `Receipt` objects.
|
|
301
|
+
- Only receipts that Számlázz.hu does not know about, such as a paper pad used during an outage, are yours to submit: `nav.submitReport(paperReceiptReport({ applicableDate, entries }))` with `allowWrite: true`.
|
|
302
|
+
|
|
303
|
+
## 14. Platform invoicing for many companies (delegated invoicing)
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { createKasszaPool } from 'kassza/delegation'
|
|
307
|
+
import { upstashRedisCookieStore } from 'kassza/cookie-stores'
|
|
308
|
+
import { Redis } from '@upstash/redis'
|
|
309
|
+
|
|
310
|
+
const redis = Redis.fromEnv()
|
|
311
|
+
|
|
312
|
+
export const pool = createKasszaPool({
|
|
313
|
+
resolve: async (merchantId) => {
|
|
314
|
+
const merchant = await loadMerchant(merchantId)
|
|
315
|
+
if (!merchant) return undefined
|
|
316
|
+
return { username: merchant.delegateUser, password: merchant.delegatePassword, invoicePrefix: merchant.prefix }
|
|
317
|
+
},
|
|
318
|
+
cookieStore: upstashRedisCookieStore(redis),
|
|
319
|
+
attemptLedger: upstashRedisCookieStore(redis),
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const kassza = await pool.get('merchant-42')
|
|
323
|
+
await kassza.invoices.createOnce({ orderNumber: 'BOOKING-881', buyer, items })
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- Link each merchant once with `connectPrincipal` from `kassza/delegation`, then wait until the merchant accepts. Check with `probeDelegation` rarely: every call emails the merchant again.
|
|
327
|
+
- Store the dedicated user's password encrypted. Call `pool.forget(merchantId)` when it or the prefix changes.
|
|
328
|
+
|
|
329
|
+
## 15. Receive the Számlázz.hu financial data link (accounting or ERP systems)
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
// app/api/szamlazz/data-link/route.ts
|
|
333
|
+
import { dataLinkHandler } from 'kassza/data-link'
|
|
334
|
+
|
|
335
|
+
export const POST = dataLinkHandler({
|
|
336
|
+
verifyKey: async (key) => (await customerKeys()).includes(key),
|
|
337
|
+
onPush: async (push) => {
|
|
338
|
+
if (push.kind === 'invoice' || push.kind === 'incoming-invoice') {
|
|
339
|
+
const registrationNumber = await saveInvoice(push.id, push.invoice, push.deleted)
|
|
340
|
+
return { registrationNumber }
|
|
341
|
+
}
|
|
342
|
+
if (push.kind === 'bank-transaction') await saveTransaction(push.transaction)
|
|
343
|
+
if (push.kind === 'receipts') await saveReceipts(push.receipts)
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
- The requests are not signed, so the key check is mandatory: `keys` or `verifyKey`.
|
|
349
|
+
- Upsert invoices by `push.id`, because the same invoice arrives again after a payment or a reversal.
|
|
350
|
+
- Keep `onPush` fast (save and return). A slow or failing endpoint is retried for 72 hours and can get the whole receiving system switched off by Számlázz.hu.
|
|
351
|
+
|
|
352
|
+
## 16. Integration tests with the fake Számla Agent
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
import { createKassza } from 'kassza'
|
|
356
|
+
import { createFakeAgentFetch } from 'kassza/testing'
|
|
357
|
+
import { expect, test } from 'vitest'
|
|
358
|
+
|
|
359
|
+
test('a lost response does not create a second invoice', async () => {
|
|
360
|
+
const agent = createFakeAgentFetch()
|
|
361
|
+
const kassza = createKassza({ agentKey: 'test-key', fetch: agent.fetch, retryDelayMs: 0 })
|
|
362
|
+
agent.fail('ghostSuccess', { action: 'createInvoice' })
|
|
363
|
+
|
|
364
|
+
const result = await kassza.invoices.createOnce(
|
|
365
|
+
{ orderNumber: 'WEB-1', buyer, items },
|
|
366
|
+
{ recoveryDelayMs: 0 },
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
expect(result.created).toBe(true)
|
|
370
|
+
expect(agent.invoices.size).toBe(1)
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The fake Agent runs the real kassza code (XML, multipart, response parsing, retries, `createOnce` recovery) against an in-memory Számla Agent. `agent.requests` shows every request kassza sent.
|
|
375
|
+
|
|
376
|
+
## 17. Invoicing from an AI assistant (MCP)
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"mcpServers": {
|
|
381
|
+
"kassza": {
|
|
382
|
+
"command": "npx",
|
|
383
|
+
"args": ["-y", "kassza", "mcp"],
|
|
384
|
+
"env": { "SZAMLAZZ_AGENT_KEY": "..." }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- The server is read-only by default. Add `"--allow-write"` to `args` to enable issuing and reversing; even then every write needs the confirmation code from the matching preview tool, for exactly the same input.
|
|
391
|
+
- Start with a Számlázz.hu test account.
|
|
392
|
+
|
|
393
|
+
## 18. Receipt journal for the NAV daily summary
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { createKassza } from 'kassza'
|
|
397
|
+
import { createJournal, kvJournal } from 'kassza/journal'
|
|
398
|
+
import { navDailyReports } from 'kassza/reports'
|
|
399
|
+
import { upstashRedisStore } from 'kassza/stores'
|
|
400
|
+
|
|
401
|
+
const store = upstashRedisStore(Redis.fromEnv())
|
|
402
|
+
export const journal = createJournal(kvJournal(store))
|
|
403
|
+
export const kassza = createKassza({
|
|
404
|
+
createOnceLock: store,
|
|
405
|
+
hooks: { onDocument: (event) => journal.record(event), onDocumentError: 'throw' },
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
await journal.trackReceipt(order.id, () => kassza.receipts.createOnce({ orderNumber: order.id, items }))
|
|
409
|
+
const reports = navDailyReports(await journal.receipts({ from: '2026-10-01', to: '2026-10-31' }))
|
|
410
|
+
await journal.settle(kassza)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
- `trackReceipt` leaves a pending entry when the outcome is unknown; schedule `journal.settle(kassza)` (for example daily) to resolve them.
|
|
414
|
+
- `journal.reconcile(push.receipts)` with the data link archive adds receipts the journal missed.
|
|
415
|
+
|
|
416
|
+
## 19. Monthly subscription invoicing
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { billingPeriodAt, runBatch } from 'kassza/batch'
|
|
420
|
+
|
|
421
|
+
const period = billingPeriodAt({ interval: 'month', anchor: '2026-01-31', paymentDueInDays: 8 }, new Date())
|
|
422
|
+
const result = await runBatch(kassza, {
|
|
423
|
+
items: subscriptions.map((subscription) => ({
|
|
424
|
+
key: `SUB-${subscription.id}-${period.start}`,
|
|
425
|
+
document: () => ({ kind: 'invoice', input: invoiceFor(subscription, period) }),
|
|
426
|
+
})),
|
|
427
|
+
journal,
|
|
428
|
+
})
|
|
429
|
+
if (result.stopped) alertHuman(result.stopped.reason)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- Run it from your scheduler (Vercel Cron, Workers Cron). A rerun reports already issued items as `existing`.
|
|
433
|
+
- Try `dryRun: true` first: invoices are previewed by Számlázz.hu, nothing is created.
|
|
434
|
+
|
|
435
|
+
## 20. Observability without telemetry
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { combineHooks, createMetricsRegistry, observe } from 'kassza/observe'
|
|
439
|
+
|
|
440
|
+
export const metrics = createMetricsRegistry()
|
|
441
|
+
export const kassza = createKassza({
|
|
442
|
+
hooks: combineHooks(observe({ logger, tracer, metrics }), { onDocument: (event) => journal.record(event) }),
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- Expose `metrics.renderPrometheus()` on an internal endpoint. Alert on `kassza_store_errors_total` and `kassza_unexpected_response_total`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kassza
|
|
3
|
-
description: Issue Hungarian invoices, proformas and receipts through Számlázz.hu with the kassza npm package. Use when code creates, reverses, queries or emails számla, díjbekérő or nyugta documents, handles Számlázz.hu IPN webhooks, looks up Hungarian tax numbers, or mentions szamlazz.hu, Számla Agent or SZAMLAZZ_AGENT_KEY.
|
|
3
|
+
description: Issue Hungarian invoices, proformas and receipts through Számlázz.hu with the kassza npm package. Use when code creates, reverses, queries or emails számla, díjbekérő or nyugta documents, turns Stripe, SimplePay, Barion, Revolut or PayPal payments into documents, handles Számlázz.hu IPN or data link webhooks, builds NAV daily receipt reports, invoices on behalf of other companies (megbízott számlázás), looks up Hungarian tax numbers, or mentions szamlazz.hu, Számla Agent or SZAMLAZZ_AGENT_KEY.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# kassza: Számlázz.hu integration
|
|
@@ -9,21 +9,27 @@ Before writing code, read the package docs from `node_modules/kassza/agents/`:
|
|
|
9
9
|
|
|
10
10
|
- `pitfalls.md`: hard rules, always read it.
|
|
11
11
|
- `api.md`: exact method signatures.
|
|
12
|
-
- `recipes.md`: patterns for webhooks, receipts, IPN, PDF storage, serverless and tests.
|
|
12
|
+
- `recipes.md`: patterns for webhooks, payment providers, receipts, IPN, NAV reports, delegated invoicing, PDF storage, serverless and tests.
|
|
13
13
|
|
|
14
14
|
## Non-negotiable rules
|
|
15
15
|
|
|
16
16
|
1. Create one server-side client with `createKassza()`. The key comes from `SZAMLAZZ_AGENT_KEY`. Never import kassza in client-side code.
|
|
17
|
-
2. Set `orderNumber` on every invoice and `callId` on
|
|
17
|
+
2. Set `orderNumber` on every invoice and receipt (and `callId` on receipts), derived from the order ID.
|
|
18
18
|
3. Give prices as `netUnitPrice` (B2B) or `grossUnitPrice` (B2C) together with `vat`. Do not compute item amounts, and do not pass UTC date strings.
|
|
19
|
-
4. Never retry `invoices.create` or `receipts.create` yourself.
|
|
19
|
+
4. Never retry `invoices.create` or `receipts.create` yourself. Prefer `invoices.createOnce()` and `receipts.createOnce()`. With plain `create`, on an error with category `network`, `timeout`, `partial_success` or `duplicate`, call `kassza.invoices.find({ orderNumber })` before doing anything else.
|
|
20
20
|
5. Branch on `SzamlazzError.category` and `code`, not on message text.
|
|
21
|
-
6. Tests must use `createMockKassza()` from `kassza/testing`, never a real Agent key.
|
|
21
|
+
6. Tests must use `createMockKassza()` or `createFakeAgentFetch()` from `kassza/testing`, never a real Agent key.
|
|
22
|
+
7. Issue Agent receipts only for activities without the online cash register obligation, and only when all four receipt conditions hold (`chooseDocument()` checks them). A fixed shop, café or restaurant needs an online cash register instead.
|
|
23
|
+
8. Never submit NAV receipt reports for receipts issued in Számlázz.hu; it reports them itself. Use `kassza/nav` read-only unless the user explicitly reports a paper receipt pad.
|
|
24
|
+
9. In serverless or multi-instance deployments, pass a shared `createOnceLock` store from `kassza/stores` (Redis, Upstash or Durable Object; not Cloudflare KV), so two instances cannot issue the same order twice.
|
|
25
|
+
10. When `onDocument` writes a journal used for NAV reports (`kassza/journal`), set `onDocumentError: 'throw'` so a missed write is never silent.
|
|
26
|
+
11. Under Express or NestJS, wrap webhook handlers with `toNodeHandler` from `kassza/node` and mount them before `express.json()`.
|
|
22
27
|
|
|
23
28
|
## Checklist before finishing
|
|
24
29
|
|
|
25
|
-
- [ ] The webhook or job that issues documents is idempotent: it runs `find` first, or catches `duplicate`.
|
|
30
|
+
- [ ] The webhook or job that issues documents is idempotent: it uses `createOnce`, runs `find` first, or catches `duplicate`.
|
|
31
|
+
- [ ] Payment provider webhooks use the `kassza/payments` handlers with `issueForPayment`, and non-retryable errors are handed to a human instead of being rethrown forever.
|
|
26
32
|
- [ ] The PDF is stored (`kassza/storage`) or re-fetched with `invoices.getPdf`, not stored as base64 in the database.
|
|
27
33
|
- [ ] The IPN route returns HTTP 200 (`ipnOkResponse()`) and processes notifications idempotently.
|
|
28
|
-
- [ ] In serverless or edge environments, a shared `cookieStore` from `kassza/cookie-stores` is configured.
|
|
29
|
-
- [ ] There is a unit test with `createMockKassza()`.
|
|
34
|
+
- [ ] In serverless or edge environments, a shared `cookieStore` (and, with several processes, an `attemptLedger`) from `kassza/cookie-stores` is configured.
|
|
35
|
+
- [ ] There is a unit test with `createMockKassza()`, or an integration test with `createFakeAgentFetch()`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migrate-from-szamlazz-js
|
|
3
|
+
description: Migrate a codebase from the szamlazz.js npm package to kassza. Use when the user asks to replace, upgrade or migrate away from szamlazz.js, or when code imports Client, Invoice, Buyer, Seller or Item from 'szamlazz.js' and the user wants kassza instead.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Migrate from szamlazz.js to kassza
|
|
7
|
+
|
|
8
|
+
Read `node_modules/kassza/agents/pitfalls.md` and `api.md` first.
|
|
9
|
+
|
|
10
|
+
## Find the usage
|
|
11
|
+
|
|
12
|
+
Search for `from 'szamlazz.js'`, `require('szamlazz.js')`, `new Client(`, `issueInvoice(`, `getInvoiceData(` and `reverseInvoice(`. List every call site to the user before editing.
|
|
13
|
+
|
|
14
|
+
## Mapping
|
|
15
|
+
|
|
16
|
+
| szamlazz.js | kassza |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `new Client({ authToken })` | one shared `createKassza({ agentKey })` (or `SZAMLAZZ_AGENT_KEY`) |
|
|
19
|
+
| `requestInvoiceDownload`, `eInvoice`, `timeout` | `downloadPdf`, `eInvoice` (per invoice or in `defaults.invoice`), `timeoutMs` |
|
|
20
|
+
| `new Seller({ bank: { name, accountNumber }, email: { replyToAddress, subject, message } })` | `seller: { bank, bankAccount, emailReplyTo, emailSubject, emailText }` |
|
|
21
|
+
| `new Buyer({ ..., postAddress })` | plain object `buyer: { name, zip, city, address, taxNumber?, postal? }` |
|
|
22
|
+
| `new Item({ label, ... })` | `{ name, quantity, unit, vat, netUnitPrice | grossUnitPrice }` |
|
|
23
|
+
| `client.issueInvoice(new Invoice({...}))` | `kassza.invoices.createOnce({ orderNumber, ... })` |
|
|
24
|
+
| `prepaymentInvoice: true` | `type: 'advance'` |
|
|
25
|
+
| `adjustmentInvoiceNumber` | `type: 'corrective', correctedInvoiceNumber` |
|
|
26
|
+
| `PaymentMethods.*`, `Currencies.*`, `Languages.*` | plain strings: `'átutalás'`, `'HUF'`, `'hu'` |
|
|
27
|
+
| `getInvoiceData({ invoiceId | orderNumber, pdf })` | `invoices.get(number | { orderNumber }, { includePdf })` or `find(...)` |
|
|
28
|
+
| `reverseInvoice({ invoiceId, eInvoice, requestInvoiceDownload })` | `invoices.reverse({ invoiceNumber, eInvoice, downloadPdf })` |
|
|
29
|
+
| result `invoiceId`, string totals, `customerAccountUrl`, Buffer `pdf` | `number`, numeric totals, `buyerAccountUrl`, `Uint8Array` `pdf` |
|
|
30
|
+
|
|
31
|
+
## Rules while migrating
|
|
32
|
+
|
|
33
|
+
1. Every invoice gets an `orderNumber` from the user's order ID, and is issued with `createOnce`. Ask if no stable order ID exists.
|
|
34
|
+
2. Delete any custom retry loop around invoice creation. kassza never resends a create.
|
|
35
|
+
3. Keep `netUnitPrice` or `grossUnitPrice` as the user had it; never convert prices by hand.
|
|
36
|
+
4. Replace string totals parsing (`Number(result.grossTotal)`) with the numeric fields.
|
|
37
|
+
5. Replace error handling on message text with `isSzamlazzError(error)` and `error.category`.
|
|
38
|
+
6. Update tests to `createMockKassza()` or `createFakeAgentFetch()` from `kassza/testing`.
|
|
39
|
+
7. Remove `szamlazz.js` from `package.json` only after the user confirms every call site was migrated.
|