fix(kitchen-service): harden derived ticket identities

This commit is contained in:
José René White Enciso 2026-03-31 20:21:41 -06:00
parent 0dc1f39dd2
commit 5564c06dd6
3 changed files with 51 additions and 4 deletions

View File

@ -18,6 +18,7 @@ This repo now orchestrates kitchen progression over persisted kitchen tickets fr
That means:
- board and queue reads come from persisted kitchen tickets plus lifecycle-derived ticket materialization for newly accepted restaurant orders
- derived kitchen IDs now use the full order identity so runtime projections stay collision-safe across similar order numbers
- claim and priority changes update persisted kitchen ticket state
- ticket transitions update both kitchen tickets and the linked restaurant order lifecycle
- accepted orders can become kitchen-visible without waiting for a stack reset or a second write path

View File

@ -316,10 +316,9 @@ public sealed class DefaultKitchenWorkflowPort : IKitchenWorkflowPort
private static PersistedKitchenWorkItemRecord BuildDerivedWorkItem(PersistedRestaurantLifecycleRecord order)
{
var numericSuffix = new string(order.OrderId.Where(char.IsDigit).ToArray());
var workItemId = string.IsNullOrWhiteSpace(numericSuffix)
? $"WK-{order.OrderId}"
: $"WK-{numericSuffix}";
// Use the full order identity when deriving kitchen IDs so distinct orders
// cannot collide just because they share a numeric suffix.
var workItemId = $"WK-{order.OrderId}";
return new PersistedKitchenWorkItemRecord(
order.ContextId,

View File

@ -56,6 +56,53 @@ public class KitchenWorkflowUseCasesTests
Assert.Equal("hot-line", derivedItem.Station);
Assert.Equal("Queued", derivedItem.State);
Assert.Equal("WK-ORD-1099", derivedItem.WorkItemId);
Assert.Equal("KT-ORD-1099", derivedItem.TicketId);
}
[Fact]
public async Task GetKitchenBoardUseCase_DerivesCollisionSafeIdsForSimilarNumericSuffixes()
{
await restaurantLifecycle.UpsertOrderAsync(
new Kitchen.Service.Application.State.PersistedRestaurantLifecycleRecord(
"demo-context",
"ORD-STAGE49-001",
"CHK-ORD-STAGE49-001",
"T-31",
"Accepted",
"Open",
2,
false,
25.00m,
"USD",
"customer-orders",
new[] { "ITEM-101", "ITEM-102" },
DateTime.UtcNow),
CancellationToken.None);
await restaurantLifecycle.UpsertOrderAsync(
new Kitchen.Service.Application.State.PersistedRestaurantLifecycleRecord(
"demo-context",
"ORD-STAGE49-WAITER-001",
"CHK-ORD-STAGE49-WAITER-001",
"T-32",
"Accepted",
"Open",
3,
false,
37.50m,
"USD",
"waiter-floor",
new[] { "ITEM-201", "ITEM-202", "ITEM-203" },
DateTime.UtcNow.AddSeconds(1)),
CancellationToken.None);
var useCase = new GetKitchenBoardUseCase(workflowPort);
var response = await useCase.HandleAsync(new GetKitchenBoardRequest("demo-context"), CancellationToken.None);
var queuedLane = Assert.Single(response.Lanes, lane => lane.Lane == "queued");
Assert.Contains(queuedLane.Items, item => item.WorkItemId == "WK-ORD-STAGE49-001" && item.TicketId == "KT-ORD-STAGE49-001");
Assert.Contains(queuedLane.Items, item => item.WorkItemId == "WK-ORD-STAGE49-WAITER-001" && item.TicketId == "KT-ORD-STAGE49-WAITER-001");
}
[Fact]