fix(print): route nested bundle components to the right kitchen ticket - #87
Merged
Merged
Conversation
Backend PR #237 (issue #234) made OrderDto.Items ROOT-ONLY: a bundle's components and an item's add-on sides are no longer top-level rows, they hang off their parent in SideItems, to arbitrary depth. No DTO field changed, so the feed still deserializes — the printer app just stopped seeing most of the order. Kitchen routing read the top level only, in both of its steps. A "Menu Deal" whose product is FrontKitchen containing BackKitchen fries then produced NO back-kitchen ticket at all (hasBackKitchenItems was false), and the fries printed on the front kitchen's ticket instead, as a nested "+ 2x Fries" line. A live order line, silently sent to the wrong kitchen. Routing now walks the whole tree, in KitchenTicketFilter — a pure function, like FeedWatchdogDecision, so the tree walk is testable without a printer. A line goes to the kitchen it names; a line that names none (a drink, an extra sauce) is a modifier of its parent and rides with it. A parent whose kitchen differs from its component's still reaches that component's ticket, but only as context: printed parenthesised at normal size, so it doesn't read as a dish that kitchen has to make. The "does this kitchen get a ticket" question is now answered by the very thing that will be printed, rather than by a second scan beside it. Two further regressions from the same contract change, found while tracing it — both are cases of a child that used to be its own top-level row and therefore printed in full: - The kitchen receipt rendered children ONE level deep, so a grandchild in a nested bundle printed nowhere. Rendering is recursive now. - It rendered a child's name and quantity only, dropping its "NO onion" and its note. Every line now prints its own detail at its own depth. A parent's note also moved above its components: trailing them, it read as if it belonged to the last child printed. Order/OrderItem gain a clone helper each (used to build a ticket without mutating the order the history list and the UI hold) and OrderItem gains a [JsonIgnore] IsContextOnly presentation flag. No wire field added, renamed or removed — the DTO mirror is unchanged. The clone also retires CreateFilteredOrder's 28-line hand-copy, where a field added to Order would have silently vanished from every kitchen ticket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
CreateFilteredOrder and AppendKitchenItem touch no instance state (S2325), and the item renderer's cognitive complexity was 20 against a limit of 15 (S3776) — the ingredient-customization block is now its own method, and the two redundant "not null and Any" guards before a foreach are gone. No behaviour change: same lines, same ESC/POS sequences, same order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
mahmutkaya
added a commit
that referenced
this pull request
Jul 27, 2026
Ships #87: kitchen-ticket routing walks the nested item tree, so a bundle component whose kitchen differs from its parent's reaches its own kitchen instead of no kitchen at all. Deliberately released AHEAD of backend PR #237, which is what makes OrderDto.Items root-only and is still on backend `develop`. The fix is backward compatible: against today's production backend the printer feed returns SideItems: null on every order, so every line is a root with its own KitchenType and the filter reduces to exactly the old top-level `Where(KitchenType == kitchen)` — byte-identical receipts. The new nested layout only appears once the backend release lands, by which time the client workstations will already have auto-updated. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
develop2026-07-27) madeOrderDto.ItemsROOT-ONLY: bundle components and add-on sides are no longer top-level rows, they hang off their parent inOrderItemDto.SideItems, to arbitrary depth. No field changed, so the feed still deserializes — the printer app just stopped seeing most of the order.FrontKitchencontainingBackKitchenfries produced no back-kitchen ticket at all, and printed the fries on the front kitchen's ticket as a nested+ 2x Friesline. A live order line, silently sent to the wrong kitchen.KitchenTicketFilter, and kitchen-ticket rendering is recursive.Must ship before the backend
develop→mainrelease PR.Sprint task / issue
Type
fix— bug fixWhat changed
1. Routing recurses (
KitchenTicketFilter, new). A pure function — same shape asFeedWatchdogDecision— so the tree walk is testable without a printer.KitchenType: "None") is a modifier of its parent and rides with it wherever the parent is made. It is dropped from tickets that carry the parent only as context.2.
has{Front,Back}KitchenItemsis now derived from the filtered ticket itself rather than a second scan beside it — "does this kitchen get a ticket" is answered by the very thing that will be printed. The two used to be decided separately, and separately is how they drifted apart.3. Two further regressions from the same contract change, found while tracing it. Both are cases of a child that used to be its own top-level row and therefore printed in full:
NO onionand its note. Every line now prints its own detail at its own depth.4. A parent's
NOTE:moved above its components — trailing them, it read as if it belonged to the last child printed.Ticket output (verified against a loopback sink, ESC/POS stripped)
Order:
Menu Deal(Front, note "allergy: no nuts") →Kofte(Front, NO Onion) +Fries(Back, "extra crispy") +Ayran(no kitchen); plusAdana Kebab(Back).Each component appears exactly once, on the right ticket; the allergy note reaches both kitchens; the combo line is unmistakably context on the back ticket.
NFR triage
KitchenTicketFilterTests) + 2 print-to-sink integration through the real composer and transport. Verified non-vacuous: 4 of them fail against the pre-fix top-level-only filter.OrderPrintServicegrew by only 15 LOC net despite the new recursive renderer, becauseCreateFilteredOrder's 28-line hand-copy was retired.Acceptance criteria coverage
has*KitchenItemsrecurses intoSideItems, arbitrary depthNestedGrandchild_ReachesItsOwnKitchen_ThroughTheParentsAboveItcovers 3 levelsCreateFilteredOrderkitchen-aware for children — mismatched child does not ride on the parent's ticketMixedKitchenBundle_BothKitchensGetATicket_EachComponentOnExactlyOne[JsonIgnore] bool IsContextOnlypresentation flag — see belowWhy
IsContextOnlyexists (the one deviation from "no model change")The renderer cannot derive "is this line context" from
KitchenTypealone: a component with no kitchen of its own is made wherever its parent is, so a rode-along drink and a context-only combo both fail a plain comparison against the ticket's kitchen. My first cut did exactly that and printed(1x Ayran)— a drink the front kitchen does serve — as context on the front ticket. Caught by eyeballing a rendered ticket, now pinned by assertions. The filter knows the answer; the flag is how it tells the renderer.[JsonIgnore], never deserialized, so the DTO mirror is untouched (TypeDisplayis the existing precedent for a non-DTO member on this model).Backend contract verification
Backend DTOs referenced / mirrored:
backend/RestaurantSystem.Api/Features/Orders/Dtos/OrderItemDto.cs—List<OrderItemDto>? SideItems,string? KitchenType(FrontKitchen/BackKitchen/None),ItemKind? Kind.PrinterAPP/Models/Order.csalready mirrorsSideItems+KitchenType;Kindis still unmirrored and unused (routing keys offKitchenType, which is what the backend resolves per line).backend/RestaurantSystem.Api/Features/Orders/Services/OrderMappingService.cs—MapToOrderDtoprojects roots only and nests children viaMapOrderItem(child, childrenByParent); arbitrary depth confirmed.backend/RestaurantSystem.Api/Features/Orders/Services/OrderItemFactory.cs:92— child rows carryItemTotal = 0, the parent's total is rolled up ⇒ the cashier receipt needs no change; root-only actually removes the oldCHF 0.00child lines from it.Cross-repo impact: none — consumer-side only. No printer-app change is required in
backend/.Standard checklist
dotnet build -f net10.0-androidon macOS (the Windows TFM needs a Windows host; see risk below). No new warnings from any changed file.dotnet test PrinterAPP.Tests— 173/173 passKitchenTicketFilter, a pure static decision function (CLAUDE.md §3 precedent:FeedWatchdogDecision,PrinterTransportResolver,OrderFeedParser); no DI registration needednull!on model fieldsPrinterAPP.Tests.csprojdevelop; PR targetsdevelopTest plan
OrderPrintService+NetworkTcpTransportinto loopback sinks; asserts per-component occurrence counts per ticket, and that a kitchen with nothing to make is never contacted.dotnet build PrinterAPP.slnon a Windows host.Risk / what is NOT verified here
ESC_SIZE_WIDE/ESC_SIZE_TALL/ESC_SIZE_NORMALusage); what changed is line ordering, indentation depth, and the parenthesised context line. Worth one real ticket before the release PR.ItemKind.Kindis deliberately not mirrored. If bundle components and true add-on sides should ever print differently, that is the field to bring across — not needed for routing.Release notes