fiskaltrust / fiskaltrust/middleware
QueueAT: a rejected or unsigned monthly/yearly closing is still recorded as a successful receipt
- Dominant language
- C#
- Stars
- 20
- Forks
- 11
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 13
Description
## Summary
An AT monthly (0x05) or yearly (0x06) closing request that the queue **does not** process as a closing is still recorded as a successful receipt: it gets a receipt number and a receipt-journal row, and downstream systems treat it as a closing. The legacy queue did not do this. A closing that fails must not be considered succeeded.
## Where
`queue/src/fiskaltrust.Middleware.Localization.QueueAT/RequestCommands/MonthlyClosingReceiptCommand.cs` (the yearly command is identical):
- **Rejected closing** (line 33): when the request carries charge or pay items, the command writes the "Tried to create a monthly receipt … but the incoming receipt is not a zero receipt" action journal and returns the *unchanged* response (line 38). No error state, no flag.
- **Unsigned closing** (line 104): when `SignReceiptAsync` yields no `journalAT`, the command writes "Monthly receipt failed: Could not sign receipt", does not advance `LastSettlementMonth` / `LastSettlementMoment`, and returns the response as a normal receipt.
`queue/src/fiskaltrust.Middleware.Queue/SignProcessor.cs`: the only thing that suppresses the receipt journal is `receiptResponse.IsError()` at line 226, which is `(ftState & 0xFFFF_FFFF) == 0xEEEE_EEEE`. Neither closing branch sets that state, so both fall into the `else` at line 247: `CreateReceiptJournalAsync` increments `ftReceiptNumerator` (line 344) and inserts the row, with `ftReceiptTotal` = `cbReceiptAmount` or the sum of the charge items.
## What legacy did
`middleware-legacy/src/fiskaltrust.service.shared/worker.cs:843` gates the numerator and the receipt-journal insert on `success`, and the rejected monthly branch in `workerAT.cs:1491` returns `false` before `MonthReceiptAT`. A rejected closing left only the action journal: no receipt number, no receipt-journal row. That has been the behaviour since the closing was introduced in 2016.
## Effect downstream
- The rejected attempt consumes a receipt number and appears in the receipt journal with a non-zero total while its request says 0x05/0x06.
- helipad triggers the AT DEP export on the uploaded receipt case alone, so the rejected attempt fires an export and, until fiskaltrust/service-helipad#31, was also used as the lower bound of the *next* export, i.e. exports were anchored on a request that never closed anything. helipad#31 now skips non-zero rows for that reason, but the queue should not produce them in the first place.
- The receipt hash chain includes a receipt that the queue itself says did not happen.
## Expected
**Definition of a zero receipt.** Today only the item lists are checked (`HasChargeAndPayItems`). A closing must also require `cbReceiptAmount` to be absent or 0: a POS that sends an amount on an otherwise empty closing currently gets a signed closing whose receipt-journal total is that amount (`CreateReceiptJournalAsync` takes `cbReceiptAmount` when present, otherwise the item sum). Every consumer that identifies closings by "zero total" then has to reason about that case; refusing it at signing time removes it for good. Legacy had the same gap.
A rejected or unsigned closing is not a successful receipt: no receipt number, no receipt-journal row, the response carries an error state (or a dedicated state flag) that the POS can act on, and the action journal stays as the record of the attempt. In terms of the current code: the two branches above should return a response `SignProcessor.IsError()` recognises, or `SignProcessor` needs a second condition besides `0xEEEE_EEEE` for "processed but not a receipt".
Whether the unsigned case should instead be treated like any other SSCD-failed-mode receipt (journaled, but with the failed-mode state and without settling the month) is a product decision; the rejected case is not.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Contributor guide
Research direction
Start with QueueAT RequestCommands/MonthlyClosingReceiptCommand.cs and the identical yearly command, then trace receipt handling in Queue/SignProcessor.cs, especially IsError() and CreateReceiptJournalAsync. Compare the relevant behavior in middleware-legacy/service.shared/worker.cs and workerAT.cs. Done means rejected or unsigned closings do not create receipt numbers or receipt-journal rows, while preserving the action journal and an actionable error or agreed failed-mode state; the unsigned behavior remains a product decision.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100