kassza 0.12.0 → 0.13.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.md +283 -18
- package/agents/README.md +20 -5
- package/agents/api.md +165 -2
- package/agents/pitfalls.md +16 -3
- package/agents/recipes.md +215 -45
- package/agents/skills/kassza/SKILL.md +11 -8
- package/dist/cli.cjs +552 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +552 -0
- package/dist/client-6MkeuFqD.d.ts +268 -0
- package/dist/client-B5FryKxJ.d.cts +268 -0
- package/dist/{client-CsmGIFRS.cjs → client-C-PdfL_P.cjs} +235 -597
- package/dist/{client-CL3fuH6A.js → client-CfaS1-Jx.js} +35 -403
- package/dist/context-BxQlIlug.d.ts +425 -0
- package/dist/context-DXNbTPwz.d.cts +425 -0
- package/dist/{create-once-CqdYmQD8.js → create-once-Cz621l8r.js} +3 -3
- package/dist/{create-once-D0pwuenl.cjs → create-once-DgMtKKcG.cjs} +8 -2
- 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 +79 -0
- package/dist/data-link/index.d.ts +79 -0
- package/dist/data-link/index.js +224 -0
- package/dist/delegation/index.cjs +34 -33
- package/dist/delegation/index.d.cts +2 -1
- package/dist/delegation/index.d.ts +2 -1
- package/dist/delegation/index.js +3 -2
- package/dist/get-Byc9T5WO.d.cts +151 -0
- package/dist/get-Byc9T5WO.d.ts +151 -0
- package/dist/get-Csm2n0rZ.cjs +445 -0
- package/dist/get-DT3JEgQ6.js +380 -0
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -3
- package/dist/{issue-BsyYdrIG.cjs → issue-AGRohSm7.cjs} +48 -665
- package/dist/{issue-C6Fcjvln.js → issue-DtSLpVdJ.js} +5 -502
- 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 +1 -1
- package/dist/money/index.js +1 -1
- package/dist/{money-DrhGkWHW.cjs → money-BxytJW6f.cjs} +25 -9
- package/dist/{money-DAA7OwiO.js → money-t7uy1n91.js} +25 -9
- 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-BEwcX7-N.cjs +203 -0
- package/dist/nav-CK1WgYo7.js +156 -0
- package/dist/parse-BJj-aqKy.cjs +380 -0
- package/dist/parse-C608ehqM.js +291 -0
- package/dist/payments/barion.cjs +1 -1
- package/dist/payments/barion.d.cts +2 -2
- package/dist/payments/barion.d.ts +2 -2
- package/dist/payments/barion.js +1 -1
- package/dist/payments/index.cjs +2 -2
- package/dist/payments/index.d.cts +3 -3
- package/dist/payments/index.d.ts +3 -3
- package/dist/payments/index.js +2 -2
- package/dist/payments/paypal.cjs +1 -1
- package/dist/payments/paypal.d.cts +2 -2
- package/dist/payments/paypal.d.ts +2 -2
- package/dist/payments/paypal.js +1 -1
- package/dist/payments/revolut.cjs +2 -2
- package/dist/payments/revolut.d.cts +2 -2
- package/dist/payments/revolut.d.ts +2 -2
- package/dist/payments/revolut.js +2 -2
- package/dist/payments/simplepay.cjs +2 -2
- package/dist/payments/simplepay.d.cts +2 -2
- package/dist/payments/simplepay.d.ts +2 -2
- package/dist/payments/simplepay.js +2 -2
- package/dist/payments/stripe.cjs +2 -2
- package/dist/payments/stripe.d.cts +2 -2
- package/dist/payments/stripe.d.ts +2 -2
- package/dist/payments/stripe.js +2 -2
- package/dist/reports/index.cjs +11 -164
- 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-BXOww1Uq.cjs +286 -0
- package/dist/serialize-DSE_o9RK.js +215 -0
- package/dist/server-C6PoNkdD.cjs +1557 -0
- package/dist/server-Cd8HB661.js +1504 -0
- package/dist/testing/index.cjs +1476 -3
- package/dist/testing/index.d.cts +194 -2
- package/dist/testing/index.d.ts +194 -2
- package/dist/testing/index.js +1477 -6
- package/dist/{types-COf1LAD-.d.cts → types-BVeR7KyU.d.cts} +1 -1
- package/dist/{types-COf1LAD-.d.ts → types-BVeR7KyU.d.ts} +1 -1
- package/dist/{webhook-C76bV-4R.cjs → webhook-81giRMdQ.cjs} +3 -25
- package/dist/{webhook-q2OzNwd7.d.ts → webhook-CpCMHhn2.d.ts} +1 -1
- package/dist/{webhook-CfaDgcL0.d.cts → webhook-CpMJuP6c.d.cts} +1 -1
- package/dist/{webhook-D4lvtex1.js → webhook-CrEGohbg.js} +3 -25
- package/llms.txt +4 -4
- package/package.json +43 -1
- package/dist/client-CickRRFr.d.ts +0 -838
- package/dist/client-DvWZoX_E.d.cts +0 -838
package/agents/api.md
CHANGED
|
@@ -12,12 +12,14 @@ const kassza = createKassza({
|
|
|
12
12
|
username?: string, password?: string,
|
|
13
13
|
defaults?: { invoice?: InvoiceDefaults, receipt?: ReceiptDefaults },
|
|
14
14
|
cookieStore?: CookieStore | false,
|
|
15
|
+
attemptLedger?: CookieStore,
|
|
15
16
|
timeoutMs?: number,
|
|
16
17
|
maxAttempts?: number,
|
|
17
18
|
retryDelayMs?: number,
|
|
19
|
+
maintenanceCooldownMs?: number,
|
|
18
20
|
fetch?: typeof fetch,
|
|
19
21
|
endpoint?: string,
|
|
20
|
-
hooks?: { onRequest?, onResponse?, onError? },
|
|
22
|
+
hooks?: { onRequest?, onResponse?, onError?, onDocument? },
|
|
21
23
|
})
|
|
22
24
|
```
|
|
23
25
|
|
|
@@ -27,12 +29,16 @@ const kassza = createKassza({
|
|
|
27
29
|
- `timeoutMs` defaults to 60000.
|
|
28
30
|
- `maxAttempts` defaults to 3 and is capped at 5. It applies only to operations that are safe to retry.
|
|
29
31
|
- `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)`.
|
|
33
|
+
- `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.
|
|
30
35
|
|
|
31
36
|
## Invoices: `kassza.invoices`
|
|
32
37
|
|
|
33
38
|
| Method | Returns | Retried automatically |
|
|
34
39
|
|---|---|---|
|
|
35
40
|
| `create(input: CreateInvoiceInput)` | `CreatedInvoice` | never |
|
|
41
|
+
| `createOnce(input: CreateInvoiceInput, options?: CreateOnceOptions)` | `InvoiceOnceResult` | never resends; looks the invoice up instead |
|
|
36
42
|
| `preview(input: CreateInvoiceInput)` | `InvoicePreview` (PDF only, no document is created) | never |
|
|
37
43
|
| `reverse(input: string \| ReverseInvoiceOptions)` | `ReversedInvoice` | never |
|
|
38
44
|
| `registerPayment(input: RegisterPaymentInput)` | `RegisteredPayment` | only when `additive: false` |
|
|
@@ -44,6 +50,19 @@ const kassza = createKassza({
|
|
|
44
50
|
|
|
45
51
|
`InvoiceReference` is `string` (the invoice number), `{ invoiceNumber }`, `{ orderNumber }`, or `{ externalId }`. When several documents share an order number, the latest one is returned.
|
|
46
52
|
|
|
53
|
+
### createOnce
|
|
54
|
+
|
|
55
|
+
`invoices.createOnce(input, { lookupFirst?, matchOrderNumber?, recoveryDelayMs?, signal? })` makes invoicing exactly-once:
|
|
56
|
+
|
|
57
|
+
1. It looks the invoice up by external ID (and, with `matchOrderNumber`, by order number) and returns it if it exists.
|
|
58
|
+
2. Otherwise it creates the invoice.
|
|
59
|
+
3. After an uncertain failure (`network`, `timeout`, `partial_success`, `duplicate`, `unexpected_response`, `unknown`) it looks the invoice up twice more, after `recoveryDelayMs` (default 1000, doubled for the second lookup).
|
|
60
|
+
|
|
61
|
+
- `orderNumber` is required. The external ID defaults to the order number with a suffix per type: `/D` proforma, `/E` advance, `/V` final, `/H` corrective, `/SZL` delivery note, none for a normal invoice. Pass `externalId` to override it.
|
|
62
|
+
- `lookupFirst` and `matchOrderNumber` default to `true`.
|
|
63
|
+
- It returns `InvoiceOnceResult`: `{ number, created, externalId, invoice?: CreatedInvoice, details?: InvoiceDetails }`. `created` is `false` when the invoice already existed.
|
|
64
|
+
- 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.
|
|
65
|
+
|
|
47
66
|
### CreateInvoiceInput
|
|
48
67
|
|
|
49
68
|
```ts
|
|
@@ -136,6 +155,8 @@ Notes on the fields:
|
|
|
136
155
|
| Method | Returns | Retried automatically |
|
|
137
156
|
|---|---|---|
|
|
138
157
|
| `create(input: CreateReceiptInput)` | `Receipt` | only with `callId` |
|
|
158
|
+
| `createOnce(input: CreateReceiptInput, options?: CreateOnceOptions)` | `{ receipt: Receipt, created: boolean }` | only with `callId` (defaults to `orderNumber`) |
|
|
159
|
+
| `convertToInvoice(input: ConvertReceiptInput, options?: CreateOnceOptions)` | `ConvertedReceipt` | never resends; looks the invoice up instead |
|
|
139
160
|
| `reverse(input: string \| { receiptNumber, callId?, downloadPdf?, template? })` | `Receipt` | only with `callId` |
|
|
140
161
|
| `get(input: string \| { receiptNumber } \| { orderNumber }, + downloadPdf?)` | `Receipt` | yes |
|
|
141
162
|
| `find(same as get)` | `Receipt \| null` | yes |
|
|
@@ -161,9 +182,12 @@ Notes on the fields:
|
|
|
161
182
|
|
|
162
183
|
`Receipt` is `{ id, number, callId?, type: 'receipt' | 'reversal', isReversed, reversedReceiptNumber?, issueDate, paymentMethod, currency, orderNumber?, items, payments, totals: { netAmount, vatAmount, grossAmount, byVat }, pdf? }`.
|
|
163
184
|
|
|
185
|
+
- `receipts.createOnce` requires `orderNumber`, uses it as `callId` unless you give one, looks the receipt up by order number first (skip with `lookupFirst: false`), and after error 338 or another uncertain failure returns the receipt that already exists.
|
|
186
|
+
- `receipts.convertToInvoice({ receiptNumber, buyer, orderNumber?, prefix?, comment?, language?, template?, seller?, eInvoice?, downloadPdf?, vatMapping?, allowAlreadyReversed? })` reverses the receipt and issues an invoice for the same items to `buyer`, exactly once. It returns `{ receipt, reversal?, invoice: InvoiceOnceResult }`. The invoice external ID is `CONV/{receiptNumber}`. Receipt-only VAT codes (`ÁKK`, `MAA`, `EU`, `EUK`) need a `vatMapping` entry, chosen with an accountant.
|
|
187
|
+
|
|
164
188
|
## Taxpayer: `kassza.taxpayer`
|
|
165
189
|
|
|
166
|
-
`query(taxNumber: string): TaxpayerInfo` accepts `
|
|
190
|
+
`query(taxNumber: string): TaxpayerInfo` accepts `12345676`, `12345676-2-42` or `HU12345676`.
|
|
167
191
|
|
|
168
192
|
`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.'`.
|
|
169
193
|
|
|
@@ -171,6 +195,29 @@ Notes on the fields:
|
|
|
171
195
|
|
|
172
196
|
- `kassza.verifyCredentials(): Promise<boolean>` returns `false` for a wrong key and throws for account problems.
|
|
173
197
|
- `kassza.resetSession()` clears the session. Call it after changing account data in Számlázz.hu.
|
|
198
|
+
- `kassza.resetAttempts(errorOrKey)` clears the `attemptLedger` counter of an `attempt_limit` error (or of its `details.attemptKey`).
|
|
199
|
+
- `kassza.issueForPayment(payment, options?)` issues a receipt or invoice for a `PaymentEvent`, exactly once. See `kassza/payments` below.
|
|
200
|
+
|
|
201
|
+
## Receipt or invoice: `chooseDocument`
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { chooseDocument } from 'kassza'
|
|
205
|
+
|
|
206
|
+
const decision = chooseDocument({
|
|
207
|
+
grossTotal: 18_990,
|
|
208
|
+
currency: 'HUF',
|
|
209
|
+
exchangeRate?: number,
|
|
210
|
+
buyer?: { taxNumber?, euTaxNumber?, isBusiness? },
|
|
211
|
+
paidByFulfillment?: boolean,
|
|
212
|
+
invoiceRequested?: boolean,
|
|
213
|
+
cashRegisterRequired?: boolean,
|
|
214
|
+
})
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- It returns `{ type: 'receipt' | 'invoice' | 'cash-register', reasons: string[], grossTotalHuf }` and makes no network call.
|
|
218
|
+
- `invoice` means at least one of the four receipt conditions fails: a business buyer, a total of 900 000 HUF or more, not paid by fulfilment (`paidByFulfillment: false`), or `invoiceRequested: true`. `reasons` says which, in Hungarian.
|
|
219
|
+
- `cash-register` means a receipt would be due, but the activity needs an online cash register (`cashRegisterRequired: true`), so an Agent receipt must not be issued. An invoice is still allowed.
|
|
220
|
+
- Foreign currency totals need `exchangeRate`, because the limit is in HUF.
|
|
174
221
|
|
|
175
222
|
## Subpath modules
|
|
176
223
|
|
|
@@ -183,6 +230,17 @@ createMockKassza({ defaults?, taxpayers?: Record<taxpayerId, TaxpayerInfo>, cred
|
|
|
183
230
|
- A `MockKassza` has the same interface as `Kassza`, plus `calls`, `invoiceRecords`, `receiptRecords`, `failNext(method, error?)` and `reset()`.
|
|
184
231
|
- The method names used in `calls` and `failNext` look like `'invoices.create'` and `'receipts.send'`.
|
|
185
232
|
- The mock runs the real validation and rounding, and throws the same error codes: 7, 335, 338 and 339.
|
|
233
|
+
- `createMockKassza({ hooks: { onDocument } })` fires the same document events as the real client.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
createFakeAgentFetch(options?: FakeAgentOptions): FakeAgent
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- A fake Számla Agent behind a `fetch`: pass `agent.fetch` to the real `createKassza({ agentKey, fetch: agent.fetch, retryDelayMs: 0 })`. It parses the real request XML, numbers documents, applies the Számlázz.hu rules (259–261, 336–340, 363–365, 395, 152, 202, 524, 7, 335, 339, 53, 57, 3) and answers in the real response formats.
|
|
240
|
+
- `FakeAgent` is `{ fetch, requests, invoices, receipts, fail(fault, { action?, times? }), acceptDelegation(taxNumber), reset() }`.
|
|
241
|
+
- Faults: `'ghostSuccess'` (created, but the response is lost), `'timeout'`, `'networkError'`, `'serverError'`, `'maintenance'`, `'partialSuccess'` (56), `'testAccountLimit'` (167), `'duplicateCallId'` (338), `'duplicateOrderNumber'` (152), or `{ code, message?, afterSuccess? }`.
|
|
242
|
+
- Options: `now`, `agentKeys` and `users` (strict authentication), `testAccount` (default `true`), `invoicePrefixes`, `defaultInvoicePrefix` (default `KASSZA`), `receiptPrefixes`, `rejectDuplicateOrderNumbers` (`boolean` or `{ invoices?, receipts? }`), `seller`, `taxpayers`, `principals` (`{ [taxNumber]: 'owned' | 'unowned' }`), `faults`.
|
|
243
|
+
- Use the mock for business logic, and the fake Agent for webhooks, retries and `createOnce` recovery.
|
|
186
244
|
|
|
187
245
|
### `kassza/ipn`
|
|
188
246
|
|
|
@@ -205,3 +263,108 @@ createMockKassza({ defaults?, taxpayers?: Record<taxpayerId, TaxpayerInfo>, cred
|
|
|
205
263
|
- `calculateInvoiceItem(input, currency?)` and `calculateReceiptItem(input, currency?)` return `ItemAmounts`.
|
|
206
264
|
- `summarizeItems(items)` returns `{ netAmount, vatAmount, grossAmount, byVat }`.
|
|
207
265
|
- `roundMoney(value, decimals)`, `isVatRate(value)`, `NUMERIC_VAT_RATES`, `SPECIAL_VAT_CODES`.
|
|
266
|
+
|
|
267
|
+
### `kassza/storage` and `kassza/cookie-stores`
|
|
268
|
+
|
|
269
|
+
- `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`.
|
|
271
|
+
|
|
272
|
+
### `kassza/payments`
|
|
273
|
+
|
|
274
|
+
Webhook handlers are `(request: Request) => Promise<Response>`, usable directly as Next.js route handlers, in Hono or in Workers. Each one verifies the request, turns it into a `PaymentEvent` and calls `onPayment`:
|
|
275
|
+
|
|
276
|
+
| Import | Handler | Required options |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `kassza/payments/stripe` | `stripeWebhook` | `secret` (string or array for rotation); optional `apiKey` to load line items and refunds |
|
|
279
|
+
| `kassza/payments/simplepay` | `simplePayWebhook` | `secretKey` or `merchants` |
|
|
280
|
+
| `kassza/payments/barion` | `barionWebhook` | `posKey` |
|
|
281
|
+
| `kassza/payments/revolut` | `revolutWebhook` | `secret`, `apiKey` |
|
|
282
|
+
| `kassza/payments/paypal` | `payPalWebhook` | `webhookId`, `clientId`, `clientSecret` |
|
|
283
|
+
|
|
284
|
+
- Common options: `onPayment(payment)`, `onError?(error)`, `method?` (the payment method written on the document), `maxBodyBytes?`, `fetch?`; `sandbox?` where the provider has one.
|
|
285
|
+
- 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).
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const result = await kassza.issueForPayment(payment, {
|
|
290
|
+
vat?: VatRate,
|
|
291
|
+
vatFor?: (item: PaymentLineItem) => VatRate,
|
|
292
|
+
items?: PaymentDocumentItem[],
|
|
293
|
+
fallbackItemName?: string,
|
|
294
|
+
document?: 'auto' | 'receipt' | 'invoice',
|
|
295
|
+
buyer?: InvoiceBuyer,
|
|
296
|
+
buyerIsBusiness?, invoiceRequested?, cashRegisterRequired?,
|
|
297
|
+
orderNumber?: string,
|
|
298
|
+
exchangeRate?, exchangeBank?,
|
|
299
|
+
allowAmountMismatch?: boolean,
|
|
300
|
+
receipt?: Partial<CreateReceiptInput>,
|
|
301
|
+
invoice?: Partial<CreateInvoiceInput>,
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
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.
|
|
306
|
+
- The order number defaults to `{PROVIDER}-{payment id}`, for example `STRIPE-pi_123`, so the payment and its refund share it.
|
|
307
|
+
- 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
|
+
- 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 }`.
|
|
310
|
+
- `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
|
+
|
|
312
|
+
### `kassza/delegation`
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
connectPrincipal({ principal, user }, { agentKey?, verifyTaxNumber?, ... }): Promise<ConnectPrincipalResult>
|
|
316
|
+
probeDelegation({ username, password, ... }): Promise<DelegationProbeResult>
|
|
317
|
+
createDelegateKassza({ username, password, invoicePrefix, receiptPrefix?, defaults?, ... }): Kassza
|
|
318
|
+
createKasszaPool({ resolve, maxClients?, ... }): KasszaPool
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- `connectPrincipal` (the Agent call `action-agent_ceg_mb`) creates or links a principal's account. `principal` is `{ name, taxNumber, invoicePrefix, zip, city, address, email, postalAddress?, bankName?, bankAccount?, replyToEmail?, cashAccountingFrom?, cashAccountingTo?, kataFrom?, kataTo? }`; `user` is the dedicated user `{ email, password (8–128 characters), firstName, lastName? }`. The tax number must pass the checksum, and the prefix is at most 5 uppercase letters or digits.
|
|
322
|
+
- Its `status` is `'account-created'`, `'owner-invite-resent'`, `'join-request-sent'`, `'join-request-resent'` or `'unknown'`. Every call emails the principal again, so never call it in a loop.
|
|
323
|
+
- `probeDelegation` signs in as the dedicated user and returns `state`: `'active'`, `'awaiting-approval'` (error 3), `'awaiting-owner-registration'` (error 250) or `'multi-account-user'` (error 164). It also re-sends the owner invitation, so call it rarely.
|
|
324
|
+
- `createDelegateKassza` returns a normal `Kassza` that authenticates as the dedicated user and sends the principal's prefix on every document.
|
|
325
|
+
- `createKasszaPool({ resolve: (principalId) => credentials | undefined })` caches clients per principal (LRU, `maxClients` default 100): `get(principalId)`, `forget(principalId)`, `clear()`, `size`.
|
|
326
|
+
- `suggestedPrefix(error)` reads the prefix Számlázz.hu suggests in error 356.
|
|
327
|
+
|
|
328
|
+
### `kassza/reports`
|
|
329
|
+
|
|
330
|
+
- `navDailyReports(receipts: Receipt[], { includeTest?, vatCategory?, series? }): NavReceiptReport[]` builds the NAV daily receipt report: one entry per day, receipt series and currency, with `applicableDate`, `series`, `serialNumber` (the first receipt number), `currency`, `exchangeRate`, `vatCategories: [{ vat, saleDocument, modifyingDocument }]`, `total`, `numberOfSaleDocument`, `numberOfModifyingDocument`, `receiptNumbers`. Test-account receipts are skipped unless `includeTest`.
|
|
331
|
+
- `dailyClose(receipts, { includeTest? })` returns per-day totals by payment method and VAT rate.
|
|
332
|
+
- `navVatCategory(vat)` maps a VAT rate to the NAV category (`0%`, `5%`, `18%`, `27%`, `Alanyi adómentes`, `Egyéb`); `describeNavReport(report)` gives a one-line Hungarian summary.
|
|
333
|
+
|
|
334
|
+
### `kassza/nav`
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
const nav = createNavReceiptClient({
|
|
338
|
+
environment: 'test' | 'production',
|
|
339
|
+
login, password | passwordHash, signatureKey, taxNumber,
|
|
340
|
+
softwareName?, allowWrite?: boolean, timeoutMs?, fetch?,
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- Reads: `listReports({ from, to, page?, pageSize? })`, `listAllReports({ from, to })`, `getReport(id)`, `listSoftware()`, `vatCategories()`, `currencies()`.
|
|
345
|
+
- Writes (`submitReport(data)`, `modifyReport(id, data)`, `invalidateReport(id)`, `registerSoftware(name?)`) throw `write_blocked` unless `allowWrite: true`. Never submit receipts that Számlázz.hu reports.
|
|
346
|
+
- `reconcileNavReports(local, remote)` compares local daily reports (for example `navDailyReports()` output) with `listAllReports()` and returns `{ matched, mismatched, missing, unexpected }`.
|
|
347
|
+
- `paperReceiptReport({ applicableDate, currency?, exchangeRate?, entries: [{ number, vat, gross, modifying? }] })` builds the daily report of a paper receipt pad, the one thing you submit yourself.
|
|
348
|
+
- Errors are `NavReceiptError` with `category`, `code`, `hint` and field errors.
|
|
349
|
+
|
|
350
|
+
### `kassza/data-link`
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
export const POST = dataLinkHandler({
|
|
354
|
+
keys?: string[],
|
|
355
|
+
verifyKey?: (key, push) => boolean | 'KEY_ERR' | 'KEY_DEL' | Promise<...>,
|
|
356
|
+
onPush: (push) => void | { registrationNumber?, keyError? },
|
|
357
|
+
onError?, maxBodyBytes?,
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- Receives the Számlázz.hu financial data link (PUSH). Either `keys` or `verifyKey` is required, because the requests are not signed; the key is in the `X-Szamlazzhu-Key` header.
|
|
362
|
+
- `push.kind` is `'invoice'` or `'incoming-invoice'` (`{ id, invoice: InvoiceDetails, deleted }`), `'bank-transaction'` (`{ transaction }`) or `'receipts'` (`{ receipts: [{ receipt, issuerTaxNumber? }] }`).
|
|
363
|
+
- The handler builds the required response XML. Return `registrationNumber` to store your filing number for invoices. Use the `id` field as the key: the same invoice can arrive again after a payment or reversal.
|
|
364
|
+
- `400` for bad XML (Számlázz.hu retries), `500` when `onPush` throws (retries for 72 hours), `KEY_ERR` for an unknown key, `KEY_DEL` to switch the link off for that customer.
|
|
365
|
+
|
|
366
|
+
### `kassza/mcp` and the CLI
|
|
367
|
+
|
|
368
|
+
- `createKasszaMcpServer({ ...KasszaOptions, allowWrite?, confirmationSecret?, confirmationTtlMs? })` is a runtime-neutral Model Context Protocol server; `connect()` returns `{ handle(message), handleLine(line) }`.
|
|
369
|
+
- `npx kassza mcp` runs it over stdio. Tools: `preview_invoice`, `preview_receipt`, `preview_reversal`, `get_invoice`, `get_receipt`, `query_taxpayer`, `nav_daily_summary`, and with `--allow-write` (or `KASSZA_MCP_ALLOW_WRITE=1`) `create_invoice`, `create_receipt`, `reverse_invoice`, `reverse_receipt`. Write tools need the confirmation code returned by the matching preview tool for exactly the same input.
|
|
370
|
+
- Other commands: `kassza doctor` (Node, time zone, key, clock skew, session), `kassza verify`, `kassza xml preview <file.json>` (the XML kassza would send, with the key masked), `kassza invoice get`, `kassza receipt get`, `kassza nav summary --file receipts.json`. Exit codes: 0 success, 1 error, 2 usage.
|
package/agents/pitfalls.md
CHANGED
|
@@ -5,11 +5,14 @@ Read this before writing any code that issues invoices or receipts. These rules
|
|
|
5
5
|
## Hard rules
|
|
6
6
|
|
|
7
7
|
1. **Never retry creating an invoice or receipt in a loop.** Számlázz.hu bans accounts that resend requests. kassza already retries only when it is safe to do so. Do not wrap `invoices.create` in your own retry loop.
|
|
8
|
-
2. **After an uncertain failure, look the document up before creating it again.** A timeout, a network error, or error `56` (`partial_success`) can mean the invoice was created anyway. Always set an `orderNumber`, and on those errors call `kassza.invoices.find({ orderNumber })` first.
|
|
8
|
+
2. **After an uncertain failure, look the document up before creating it again.** A timeout, a network error, or error `56` (`partial_success`) can mean the invoice was created anyway. Always set an `orderNumber`, and on those errors call `kassza.invoices.find({ orderNumber })` first. `invoices.createOnce()` and `receipts.createOnce()` do this for you.
|
|
9
9
|
3. **Do not compute item amounts yourself.** Pass `netUnitPrice` (B2B) or `grossUnitPrice` (B2C) plus `vat`, and kassza applies the official rounding rules. Hand-computed floats cause errors 259–264, and on receipts 261 and 363–365.
|
|
10
10
|
4. **Never use `new Date().toISOString().slice(0, 10)` for invoice dates.** Between midnight and 02:00 Budapest time it returns yesterday, which gives error 352. Leave dates out (kassza defaults to today in `Europe/Budapest`) or pass a `Date`.
|
|
11
11
|
5. **The Agent key is a secret and must be lowercase.** Read it from `SZAMLAZZ_AGENT_KEY` on the server only, and never ship it to a browser bundle. kassza rejects keys containing uppercase letters before sending anything.
|
|
12
|
-
6. **Use the test account while developing.** It allows at most 500 invoices per 10 minutes. Never run tests against a production Agent key.
|
|
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
|
+
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
|
+
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.
|
|
13
16
|
|
|
14
17
|
## Behaviour worth knowing
|
|
15
18
|
|
|
@@ -32,7 +35,14 @@ Read this before writing any code that issues invoices or receipts. These rules
|
|
|
32
35
|
| IPN webhook | Retried every 3 minutes, at most 10 times; only the latest one per invoice is sent | Respond with HTTP 200 quickly and process idempotently |
|
|
33
36
|
| IPN source check | `isSzamlazzIp` checks the rightmost `x-forwarded-for` entry, the address your nearest proxy saw; the left side is client-controlled | Behind several proxies pass `{ trustedProxies: n }`; never feed it a header that no proxy of yours writes |
|
|
34
37
|
| `buyer.groupTaxNumber`, item `dataDeletionCode` | Documented on docs.szamlazz.hu, but missing from the downloadable `xmlszamla.xsd` (and `torloKod` from `xmlnyugtacreate.xsd`) | Use them only when needed; if you get error 57, remove them |
|
|
35
|
-
| NAV receipt data reporting | Mandatory since 2026-09-01, with a grace period until 2026-12-31
|
|
38
|
+
| NAV receipt data reporting | Mandatory since 2026-09-01, with a grace period until 2026-12-31; Számlázz.hu reports the receipts issued there after the NAV connection | Check with `navDailyReports()` and `reconcileNavReports()`; report only paper receipts yourself |
|
|
39
|
+
| Receipts and the NAV Online data connection | Számlázz.hu issues receipts only from accounts where the NAV Online data connection is set up | Set it up in Számlázz.hu before the first receipt |
|
|
40
|
+
| Error `524` | The receipt prefix is not enabled in the account | Enable it under Beállítások / Előtagok |
|
|
41
|
+
| Error `491` | KATA protection blocks documents to businesses | Leave out the buyer's tax number, or a human disables the protection |
|
|
42
|
+
| 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
|
+
| Error `167` (`rate_limit`) | Too many documents in the test account in a short time | Wait a few minutes; never retry automatically |
|
|
44
|
+
| `attempt_limit` error | The same request already failed 5 times (counted across processes with `attemptLedger`) | A human fixes the cause, then `kassza.resetAttempts(error)` |
|
|
45
|
+
| `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 |
|
|
36
46
|
|
|
37
47
|
## Error categories
|
|
38
48
|
|
|
@@ -47,6 +57,9 @@ Read this before writing any code that issues invoices or receipts. These rules
|
|
|
47
57
|
| `not_found` | The document does not exist | No |
|
|
48
58
|
| `partial_success` | The document exists, but a side effect (email) failed | No |
|
|
49
59
|
| `maintenance` | Számlázz.hu maintenance (code 1) | kassza retries lookups automatically |
|
|
60
|
+
| `rate_limit` | Too many documents in the test account (code 167) | No, wait a few minutes |
|
|
61
|
+
| `attempt_limit` | The request already failed 5 times; kassza did not send it | No, a human must act, then `resetAttempts` |
|
|
50
62
|
| `network` / `timeout` | Transport failure; the outcome is unknown for writes | Look up by `orderNumber` before trying again |
|
|
51
63
|
| `configuration` | Missing key, bad options | No |
|
|
52
64
|
| `unexpected_response` | Számlázz.hu answered in an unknown format | Report it |
|
|
65
|
+
| `unknown` | An error code kassza does not know; the original message is kept | No, read `code` and `message` |
|
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
|
|
@@ -215,3 +209,179 @@ await kassza.invoices.create({
|
|
|
215
209
|
```
|
|
216
210
|
|
|
217
211
|
The exchange rate comes from MNB automatically when `exchangeRate` is omitted. Choose the VAT code together with an accountant.
|
|
212
|
+
|
|
213
|
+
## 11. Payment provider webhook → receipt or invoice (Stripe, SimplePay, Barion, Revolut, PayPal)
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// app/api/stripe/webhook/route.ts
|
|
217
|
+
import { isSzamlazzError } from 'kassza'
|
|
218
|
+
import { stripeWebhook } from 'kassza/payments/stripe'
|
|
219
|
+
import { kassza } from '@/lib/kassza'
|
|
220
|
+
|
|
221
|
+
export const POST = stripeWebhook({
|
|
222
|
+
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
223
|
+
apiKey: process.env.STRIPE_SECRET_KEY,
|
|
224
|
+
onPayment: async (payment) => {
|
|
225
|
+
try {
|
|
226
|
+
await kassza.issueForPayment(payment, { vat: 27 })
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (isSzamlazzError(error) && !error.retryable) {
|
|
229
|
+
await parkForHuman(payment.id, `${error.category}: ${error.message}`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
throw error
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- 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.
|
|
239
|
+
- 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.
|
|
240
|
+
- The same pattern works with `simplePayWebhook`, `barionWebhook`, `revolutWebhook` and `payPalWebhook`; only the credentials differ (see [api.md](./api.md)).
|
|
241
|
+
- A full refund reverses the document. A partial refund is skipped on purpose: it needs a corrective document decided by a human.
|
|
242
|
+
- 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).
|
|
243
|
+
|
|
244
|
+
## 12. Receipt or invoice, and an invoice after a receipt
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { chooseDocument } from 'kassza'
|
|
248
|
+
|
|
249
|
+
const decision = chooseDocument({
|
|
250
|
+
grossTotal: order.total,
|
|
251
|
+
buyer: { taxNumber: order.taxNumber },
|
|
252
|
+
invoiceRequested: order.wantsInvoice,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
if (decision.type === 'receipt') {
|
|
256
|
+
await kassza.receipts.createOnce({ orderNumber: `ORDER-${order.id}`, paymentMethod: 'bankkártya', items })
|
|
257
|
+
} else {
|
|
258
|
+
await kassza.invoices.createOnce({ orderNumber: `ORDER-${order.id}`, paid: true, buyer, items })
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
When a buyer asks for an invoice after getting a receipt:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const converted = await kassza.receipts.convertToInvoice({
|
|
266
|
+
receiptNumber: 'NYGT-2026-118',
|
|
267
|
+
buyer: { name: 'Példa Kft.', zip: '1111', city: 'Budapest', address: 'Fő utca 1.', taxNumber: '12345676-2-42' },
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
converted.reversal?.number
|
|
271
|
+
converted.invoice.number
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
It reverses the receipt and invoices the same items, exactly once (external ID `CONV/{receiptNumber}`).
|
|
275
|
+
|
|
276
|
+
## 13. NAV daily receipt report: check it, do not double-report
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { createNavReceiptClient, reconcileNavReports } from 'kassza/nav'
|
|
280
|
+
import { navDailyReports } from 'kassza/reports'
|
|
281
|
+
|
|
282
|
+
const nav = createNavReceiptClient({
|
|
283
|
+
environment: 'production',
|
|
284
|
+
login: process.env.NAV_LOGIN!,
|
|
285
|
+
password: process.env.NAV_PASSWORD!,
|
|
286
|
+
signatureKey: process.env.NAV_SIGNATURE_KEY!,
|
|
287
|
+
taxNumber: process.env.NAV_TAX_NUMBER!,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const local = navDailyReports(await receiptsBetween('2026-09-01', '2026-09-30'))
|
|
291
|
+
const remote = await nav.listAllReports({ from: '2026-09-01', to: '2026-09-30' })
|
|
292
|
+
const { missing, mismatched } = reconcileNavReports(local, remote)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- 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.
|
|
296
|
+
- `receiptsBetween` is your own query over stored `Receipt` objects (from `receipts.get`, `issueForPayment` results, or the data link in recipe 15).
|
|
297
|
+
- 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`.
|
|
298
|
+
|
|
299
|
+
## 14. Platform invoicing for many companies (delegated invoicing)
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { createKasszaPool } from 'kassza/delegation'
|
|
303
|
+
import { upstashRedisCookieStore } from 'kassza/cookie-stores'
|
|
304
|
+
import { Redis } from '@upstash/redis'
|
|
305
|
+
|
|
306
|
+
const redis = Redis.fromEnv()
|
|
307
|
+
|
|
308
|
+
export const pool = createKasszaPool({
|
|
309
|
+
resolve: async (merchantId) => {
|
|
310
|
+
const merchant = await loadMerchant(merchantId)
|
|
311
|
+
if (!merchant) return undefined
|
|
312
|
+
return { username: merchant.delegateUser, password: merchant.delegatePassword, invoicePrefix: merchant.prefix }
|
|
313
|
+
},
|
|
314
|
+
cookieStore: upstashRedisCookieStore(redis),
|
|
315
|
+
attemptLedger: upstashRedisCookieStore(redis),
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const kassza = await pool.get('merchant-42')
|
|
319
|
+
await kassza.invoices.createOnce({ orderNumber: 'BOOKING-881', buyer, items })
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
- 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.
|
|
323
|
+
- Store the dedicated user's password encrypted. Call `pool.forget(merchantId)` when it or the prefix changes.
|
|
324
|
+
|
|
325
|
+
## 15. Receive the Számlázz.hu financial data link (accounting or ERP systems)
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
// app/api/szamlazz/data-link/route.ts
|
|
329
|
+
import { dataLinkHandler } from 'kassza/data-link'
|
|
330
|
+
|
|
331
|
+
export const POST = dataLinkHandler({
|
|
332
|
+
verifyKey: async (key) => (await customerKeys()).includes(key),
|
|
333
|
+
onPush: async (push) => {
|
|
334
|
+
if (push.kind === 'invoice' || push.kind === 'incoming-invoice') {
|
|
335
|
+
const registrationNumber = await saveInvoice(push.id, push.invoice, push.deleted)
|
|
336
|
+
return { registrationNumber }
|
|
337
|
+
}
|
|
338
|
+
if (push.kind === 'bank-transaction') await saveTransaction(push.transaction)
|
|
339
|
+
if (push.kind === 'receipts') await saveReceipts(push.receipts)
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- The requests are not signed, so the key check is mandatory: `keys` or `verifyKey`.
|
|
345
|
+
- Upsert invoices by `push.id`, because the same invoice arrives again after a payment or a reversal.
|
|
346
|
+
- 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.
|
|
347
|
+
|
|
348
|
+
## 16. Integration tests with the fake Számla Agent
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { createKassza } from 'kassza'
|
|
352
|
+
import { createFakeAgentFetch } from 'kassza/testing'
|
|
353
|
+
import { expect, test } from 'vitest'
|
|
354
|
+
|
|
355
|
+
test('a lost response does not create a second invoice', async () => {
|
|
356
|
+
const agent = createFakeAgentFetch()
|
|
357
|
+
const kassza = createKassza({ agentKey: 'test-key', fetch: agent.fetch, retryDelayMs: 0 })
|
|
358
|
+
agent.fail('ghostSuccess', { action: 'createInvoice' })
|
|
359
|
+
|
|
360
|
+
const result = await kassza.invoices.createOnce(
|
|
361
|
+
{ orderNumber: 'WEB-1', buyer, items },
|
|
362
|
+
{ recoveryDelayMs: 0 },
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
expect(result.created).toBe(true)
|
|
366
|
+
expect(agent.invoices.size).toBe(1)
|
|
367
|
+
})
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
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.
|
|
371
|
+
|
|
372
|
+
## 17. Invoicing from an AI assistant (MCP)
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"mcpServers": {
|
|
377
|
+
"kassza": {
|
|
378
|
+
"command": "npx",
|
|
379
|
+
"args": ["-y", "kassza", "mcp"],
|
|
380
|
+
"env": { "SZAMLAZZ_AGENT_KEY": "..." }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- 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.
|
|
387
|
+
- Start with a Számlázz.hu test account.
|
|
@@ -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,24 @@ 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.
|
|
22
24
|
|
|
23
25
|
## Checklist before finishing
|
|
24
26
|
|
|
25
|
-
- [ ] The webhook or job that issues documents is idempotent: it runs `find` first, or catches `duplicate`.
|
|
27
|
+
- [ ] The webhook or job that issues documents is idempotent: it uses `createOnce`, runs `find` first, or catches `duplicate`.
|
|
28
|
+
- [ ] 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
29
|
- [ ] The PDF is stored (`kassza/storage`) or re-fetched with `invoices.getPdf`, not stored as base64 in the database.
|
|
27
30
|
- [ ] 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()`.
|
|
31
|
+
- [ ] In serverless or edge environments, a shared `cookieStore` (and, with several processes, an `attemptLedger`) from `kassza/cookie-stores` is configured.
|
|
32
|
+
- [ ] There is a unit test with `createMockKassza()`, or an integration test with `createFakeAgentFetch()`.
|