fiskaltrust / fiskaltrust/middleware

BE/ZwarteDoos: sales rejected by the Black Blox FDM (INVALID_LENGTH on SaleInput.employeeId) because the SCU sends the literal string "undefined" when cbUser is absent — start receipts still sign

Open
#759 1 comment 0 reactions 0 assignees View on GitHub
category-bug market-be
Dominant language
C#
Stars
20
Forks
11
Avg merge
1d 18h
Merged PRs (30d)
13

Description

## Symptom

On the BE sandbox (`portal-sandbox.fiskaltrust.be`), a CloudCashbox + ZwarteDoos developer-sample queue signs the **START** receipt fine but every **SALE** is accepted into the queue and then left **unsigned**: the Black Blox (FDM) rejects the sign request.

| | START (`0x4001`) | SALE (`0x0001`) |
|---|---|---|
| HTTP | 201 | 201 |
| `ftState` | `0x4245200000000000` (clean) | `0x42452000eeeeeeee` (`0xEEEEEEEE` failure marker) |
| signature | caption `Initial-operation receipt` | caption `FAILURE` |
| `ftStateData` | *(empty)* | full ZwarteDoos request/response |

Verbatim failure signature on the sale:

```
Die Anfrage an die Black Blox FDM02030462 ist ungültig.
```

The receipt viewer renders the sale with the red **REÇU INVALIDE / INVALID RECEIPT** banner.

### Run / identifiers

- e2e run: https://github.com/fiskaltrust/service-portal-ui/actions/runs/33421925256 (market `be`, scenario `developer-sample`, session `b4e4288d`)
- Queue (`ftQueueID`): `11161381-f46e-48ca-892a-88b7c6ffd48b`
- CashBox (`ftCashBoxID`): `1dc88ea2-d615-4037-8326-402c9cfcf0dc`
- FDM `deviceId`: `FDM02030462`
- START item (OK): `87b1e12b-bd80-4d6b-9c8b-5a092ebd0fff` — https://receipts-sandbox.fiskaltrust.eu/11161381-f46e-48ca-892a-88b7c6ffd48b/87b1e12b-bd80-4d6b-9c8b-5a092ebd0fff
- SALE item (FAILED): `77f27375-55b7-4fe8-8df1-f3bd518811a6` — https://receipts-sandbox.fiskaltrust.eu/11161381-f46e-48ca-892a-88b7c6ffd48b/77f27375-55b7-4fe8-8df1-f3bd518811a6
- Daily closing item (FAILED, different cause — see §2): `7a206db0-bbc0-4805-9a2f-b5ed55427ae4`

**This blocks the BE `developer-sample` e2e scenario.** Phases 01 (poscreator + POS system) and 02 (CloudCashbox technical rollout) pass; phase 03 `03-send-receipts` fails at steps 4 (sale), 5 (refund) and 6 (daily closing).

---

## Root cause (1): `employeeId` is sent as the literal string `"undefined"`

The failing receipt's own `ftStateData` contains both the request the SCU sent and the FDM's structured error. Trimmed to the relevant parts (append `?format=yaml` to the receipt URL above to reproduce):

```jsonc
"apiRequest": {
...
"language": "DE",
"vatNo": "BE0000000097",
"estNo": "2000000042",
"posId": "CPOS0031234567",
"deviceId": "FDM02030462",
"employeeId": "undefined" // <-- literal 9-char string
},
"apiResponse": {
"data": null,
"errors": [{
"message": "Die Anfrage an die Black Blox FDM02030462 ist ungültig.",
"extensions": {
"category": "FDM",
"code": "INVALID_REQUEST",
"data": [
{ "name": "SUBCATEGORY", "value": "GRAPHQL" },
{ "name": "SUBCODE", "value": "INVALID_LENGTH" },
{ "name": "INPUT_OBJECT_NAME", "value": "SaleInput" },
{ "name": "INPUT_FIELD_NAME", "value": "employeeId" },
{ "name": "EXPECTED", "value": "11" },
{ "name": "ENCOUNTERED", "value": "9" }
]
}
}]
}
```

The FDM is explicit: `SaleInput.employeeId` must be **11** characters, it got **9** — i.e. `"undefined"`.

That value is produced here, in
[`scu-be/src/fiskaltrust.Middleware.SCU.BE.ZwarteDoos/ZwarteDoosScuBe.cs`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/scu-be/src/fiskaltrust.Middleware.SCU.BE.ZwarteDoos/ZwarteDoosScuBe.cs#L245-L285):

```csharp
// ZwarteDoosScuBe.cs:239 (GetBaseData)
EmployeeId = GetEmployeeIdFromCbUser(receiptRequest.cbUser),

// ZwarteDoosScuBe.cs:245-250
private string GetEmployeeIdFromCbUser(object? cbUser)
{
if (cbUser == null)
{
return "undefined"; // <-- line 249
}
...
return "undefined"; // <-- line 284 (catch-all)
}
```

The e2e sale request has `cbUser` empty/null, so line 249 fires and the SCU puts `"undefined"` on the wire.

`"undefined"` can **never** be a valid value. The requirement — and the correct fallbacks — are documented on the field itself, in
[`Models/Shared/BaseInputData.cs:109-116`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/scu-be/src/fiskaltrust.Middleware.SCU.BE.ZwarteDoos/Models/Shared/BaseInputData.cs#L109-L116):

> The social security number of the POS operator recording the transaction. **This number contains 11 digits** in the range 0 through 9. The last two digits are a modulo 97 checksum […]
>
> An operator foreign to the business (i.e. a technician of the POS supplier) who registers transactions (i.e. for testing or troubleshooting purposes) **must be identified with number `00000000097`**. Online orders, kiosk orders, and all future possible order input systems that do not require a physical operator […] have to be identified with **"Robot-User" `00000000029`**.

Neither `00000000097` nor `00000000029` appears anywhere in the codebase — `grep -rn "00000000097\|00000000029" --include=*.cs .` matches only that doc comment. The integration tests all hardcode a valid 11-digit INSZ (`75061189702`), which is why this never showed up in CI.

There is also **no validation** on the non-null path: a caller-supplied `cbUser` of any length (including `""`) is forwarded verbatim and will produce the same opaque FDM error.

### Where the German string comes from

It is **not** emitted by any fiskaltrust code — `grep -rn "ist ungültig\|Black Blox"` over this repo returns nothing. It is the ZwarteDoos/FDM GraphQL backend's own `errors[0].message`, relayed verbatim by
[`ZwarteDoosScuBe.cs:123-127`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/scu-be/src/fiskaltrust.Middleware.SCU.BE.ZwarteDoos/ZwarteDoosScuBe.cs#L123-L127):

```csharp
if (apiResponse.Errors != null && apiResponse.Errors.Count > 0)
{
var errorMessages = string.Join("; ", apiResponse.Errors.Select(e => e.Message));
receiptResponse.SetReceiptResponseErrored(errorMessages);
return receiptResponse;
}
```

`SetReceiptResponseErrored` ([`Helpers/ReceiptResponseHelpers.cs:22-32`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/scu-be/src/fiskaltrust.Middleware.SCU.BE.ZwarteDoos/Helpers/ReceiptResponseHelpers.cs#L22-L32)) sets `State.Error` — that is the `0xEEEEEEEE` in `0x42452000eeeeeeee` — and adds the `FAILURE` signature carrying that message.

It is **German** because the SCU sends `language: "DE"`, taken from `ZwarteDoosScuConfiguration.Language` (default is `NL`), so the developer-sample BE cashbox template is configured with `Language = DE`. Cosmetic, but confusing for a BE market and worth a separate look at the template.

Note that the middleware **discards the structured detail** when surfacing the failure: the `INVALID_LENGTH` / `SaleInput.employeeId` / `EXPECTED 11` / `ENCOUNTERED 9` extensions are only in `ftStateData`; the `FAILURE` signature the POS sees is the generic vendor sentence. Only the raw `ftStateData` made this diagnosable.

## Why START passes and SALE fails

Not a device-state or session-ordering effect — **the START receipt never reaches the FDM at all.**

The BE queue routes lifecycle receipts locally:
[`queue/src/fiskaltrust.Middleware.Localization.QueueBE/Processors/LifecycleCommandProcessorBE.cs:13-21`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/queue/src/fiskaltrust.Middleware.Localization.QueueBE/Processors/LifecycleCommandProcessorBE.cs#L13-L21) — `InitialOperationReceipt0x4001Async` only activates the queue and adds a locally built signature. It has no `IBESSCD` dependency and makes no ZwarteDoos call.

Only `PointOfSaleReceipt0x0001Async` (and `UnknownReceipt0x0000Async`, which delegates to it) calls the SCU:
[`Processors/ReceiptCommandProcessorBE.cs:20-29`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/queue/src/fiskaltrust.Middleware.Localization.QueueBE/Processors/ReceiptCommandProcessorBE.cs#L20-L29).

This is confirmed by the receipts themselves: the START receipt's `ftStateData` is **empty** (no `apiRequest`/`apiResponse` block at all) and its signature is `Initial-operation receipt / Queue-ID: 11161381-…`. The sale's `ftStateData` carries the full ZwarteDoos exchange. So the FDM is contacted for the first time on the sale — and rejects it on the very first attempt.

(Side note: `ZwarteDoosScuBe.ProcessReceiptAsync` lines 49-62 contain `0x4001` / `0x4002` / `0x2000` branches that are unreachable, because the queue handles those cases itself and never delegates them.)

## Root cause (2, independent): `DailyClosing0x2011` is never routed to the SCU

Step 6 of the same phase failed with a **different** message:

> `The ftReceiptCase DailyClosing0x2011 - 0x0000000000002011 is not yet implemented in the current implementation.`

That comes from [`FallBackOperations.NotYetImplemented`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/queue/src/fiskaltrust.Middleware.Localization.v2/Helpers/FallBackOperations.cs#L12), wired up at
[`Processors/DailyOperationsCommandProcessorBE.cs:14`](https://github.com/fiskaltrust/middleware/blob/d20d080e5d5cfa9d5eee56c72614dd77c5762a8f/queue/src/fiskaltrust.Middleware.Localization.QueueBE/Processors/DailyOperationsCommandProcessorBE.cs#L14):

```csharp
public async Task DailyClosing0x2011Async(ProcessCommandRequest request)
=> await FallBackOperations.NotYetImplemented(request);
```

…even though `ZwarteDoosScuBe.PerformDailyCosing` **is** implemented (`ZwarteDoosScuBe.cs:287-343`, `ReportTurnoverZAsync`) and `ProcessReceiptAsync` has a `DailyClosing0x2011` branch at line 64. The SCU implementation is simply unreachable — the queue processor is not wired to it. Fixing (1) alone will **not** green step 6.

(Step 5, the refund, failed purely as a consequence of (1): the harness refuses to send a reversal whose `cbPreviousReceiptReference` cannot resolve to a signed sale. That enforcement behaved correctly.)

## What is established vs. what is not

**Established (high confidence, from the receipt payload + source):**
- The exact rejected field, expected vs. actual length, and the exact line of code that produces `"undefined"`.
- That the German sentence is the vendor backend's message, relayed verbatim, and why it is German.
- That START never touches the FDM, so it cannot be an FDM session/ordering problem.
- That daily closing is a separate wiring gap.

**Not established:**
- Whether the FDM would accept the request once `employeeId` is valid — everything after the `employeeId` length check is untested for this queue. The FDM validates and fails fast, so any *further* request-shape problem (VAT label `A`, `vatNo BE0000000097`, `estNo 2000000042`, `bookingPeriodId` set to the `ftQueueItemID` GUID, `posSwVersion "1.3.0"`) is masked and unverified. Fixing `employeeId` may expose a next error.
- Whether an `employeeId` of `00000000029` / `00000000097` is the *policy*-correct substitution for an operator-less receipt, or whether the middleware should instead reject the receipt with a clear middleware-side error naming `cbUser`. That is a product/compliance call, not something the source settles.
- Whether the BE `developer-sample` rollout template ought to supply an `employeeId` (or a `DefaultEmployeeId` SCU setting) rather than relying on the POS to send `cbUser` — the template lives outside this repo.

**Contributory (not the defect):** the caller sent no `cbUser`. BE genuinely requires an operator INSZ, so an argument exists that the POS should send one. But the middleware's handling of its absence is wrong either way: it silently substitutes a value that is guaranteed to be rejected, and surfaces an opaque vendor sentence instead of naming the missing input.

**Verdict:** code defect in this repo, high confidence — not a sandbox device fault and not bad test data. (The earlier e2e note that "the same rejection was already present on the queue's prior receipt, so the device has been rejecting before this step" is consistent with this: *every* sale on this queue carries the same `cbUser`-less payload, so every one hits the same deterministic validation failure.)

## Suggested next steps

1. **`GetEmployeeIdFromCbUser` (`ZwarteDoosScuBe.cs:245`)** — stop returning `"undefined"`. Options, in order of preference:
- Add an optional `DefaultEmployeeId` to `ZwarteDoosScuConfiguration`, validated as 11 digits, used when `cbUser` is absent.
- Fall back to the documented sentinel (`00000000029` Robot-User, or `00000000097` for supplier/technician traffic) — pending the compliance call above.
- Validate the resolved value (11 digits, mod-97 checksum) **before** the API call and fail with an explicit middleware error naming `cbUser` when it does not hold, instead of round-tripping to the FDM for an opaque answer.
2. **Surface the structured FDM error.** In `ZwarteDoosScuBe.cs:125`, include `extensions.code` / `SUBCODE` / `INPUT_FIELD_NAME` / `EXPECTED` / `ENCOUNTERED` in the `FAILURE` signature data, so a POS integrator does not need `ftStateData` to know which field was wrong.
3. **Wire `DailyClosing0x2011Async`** in `DailyOperationsCommandProcessorBE` to `IBESSCD.ProcessReceiptAsync`, mirroring `ReceiptCommandProcessorBE`, so the existing `PerformDailyCosing` is actually reachable. Consider the same for the other `NotYetImplemented` daily-operation cases.
4. **Add a regression test** covering `cbUser == null` on a BE `0x0001` sale, asserting the outgoing `SaleInput.employeeId` is 11 digits. Today every integration test hardcodes `EmployeeId = "75061189702"`, which is exactly why this path is untested.
5. **Re-run** the BE `developer-sample` phase 03 afterwards and expect the *next* FDM validation error, if any, to surface — see "not established" above.
6. Optionally, check why the developer-sample BE cashbox sets `Language = DE` rather than `NL`/`FR`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Contributor guide

Open the contributing guide

Research direction

Start in scu-be/src/fiskaltrust.Middleware.SCU.BE.ZwarteDoos/ZwarteDoosScuBe.cs, especially GetEmployeeIdFromCbUser and the receipt error handling, then inspect DailyOperationsCommandProcessorBE.cs and the referenced daily-closing implementation. Run the BE developer-sample e2e scenario and use the receipt ftStateData to verify the employeeId handling and daily-closing routing. Done means operator-less requests receive an agreed valid or explicit middleware error and daily closing reaches the SCU.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, graphql
Domain
api, backend, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.