📋 Async IBD ingestion — code review
0 / 136 Polski UX guide Engineering
Wholesale · Deliveries · pull request

Async inbound-delivery ingestion — review guide

A guided, ordered walkthrough of every .cs file in this change vs main — what to verify per file and a risk rating — so the review can be split across the team and nothing is missed.

136 .cs files121 new · 15 modified18 review stages 42 high47 medium46 low
Scope. The real PR — branch vs main: 13 commits, 136 .cs files (121 new, 15 modified). Review top to bottom; each stage builds on the one before.

How to use it

Review top to bottom — model → ports → orchestration → edges → tests. Tick a file once reviewed; progress is saved in your browser.

Split the work

Hand each colleague one or more stages. Start the high-risk stages (7–10: the base handler, step handlers, control commands) with your strongest reviewers.

What "checks" mean

Each file lists the specific things to verify — guard conditions, transactions, concurrency, idempotency, error classification — not a summary. Read the code; use these as the lens.

1

Domain — model

7 files2 high

Focus: The ingestion aggregate, the status/step value objects, and the queued event. Read this first — every handler builds on this state machine.

src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/Events/InboundDeliveryIngestionQueuedEvent.cshigh
Domain event fired on Create, Start, and RestartFromBeginning; carries the attempt number to arm background jobs with idempotency guard (only process if job's attempt matches aggregate's).
  • Line 8-10: Event captures Attempt at publish time; verify the event is published AFTER UpdateField bumps AttemptNumber (line 253, then 259 in RestartFromBeginning)—if order is wrong, jobs see stale attempt and ignore the work
  • Line 16: Attempt field is public set; ensure no deserialization or late-mutation of this field corrupts job deduplication
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/InboundDeliveryIngestion.cshigh
Orchestrates the long-running async ingestion pipeline: tracks status/step/attempt, persists grace-period window with token-based stale-job immunity, audits every step, and coordinates transitions between Running/Grace/Stopped/Succeeded states.
  • Line 173-188 (MarkTerminallyFailed): Verify the guard condition StatusCode list covers all non-terminal-failure states; watch that a race between in-flight step handler + terminal-failure watchdog doesn't double-record a step for the current attempt
  • Line 341 (ReachedGracePhase): Confirm HasStepSucceeded(AwaitGracePeriodCode) is correct heuristic—if AwaitGracePeriod can be skipped or if its row can be missing, this breaks grace-pause Resume
  • Line 253 (RestartFromBeginning): Marker is logged at old AttemptNumber before bump; verify that old in-flight jobs see old attempt on line 259's event and correctly no-op via attempt guard
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/IngestionOrigin.cslow
Lightweight value object for submission source (WholeSale/PartnerApp/Acquisition); used for audit trail and filtering.
  • Line 24: Single() on FromCode; verify no unused code values and that deserialization of old records doesn't drift
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/IngestionStatus.csmedium
Value object defining the seven ingestion lifecycle states (Queued/Running/Failed/Succeeded/IboGenerated/Stopped/InGracePeriod) and their constants; InProgressCodes array controls which statuses the UI and guards treat as 'active'.
  • Line 52-53: Verify InProgressCodes includes all states where a user can still act (Stopped, InGracePeriod, IboGenerated all present?); check that guards like EnsureCanBeStopped use this array correctly or have equivalent logic
  • FromCode line 55: Single() will throw if a status code is orphaned (e.g., deserialized from old DB row); verify migration guards against code values being reused or removed
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/IngestionStepExecution.csmedium
Append-only immutable audit entity for each step execution; supports idempotency checks (HasStepCompleted) and error capture (JSON payload).
  • Line 55 (FinishedAtUtc = DateTime.UtcNow): Verify clock skew or time-travel doesn't allow FinishedAtUtc < StartedAtUtc (critical for audit trail integrity)
  • Line 46 & 56: errorPayload is stored but never validated; confirm the JSON shape (Dict<Guid, Dict<string, List<string>>> vs small {message} object) is enforced/documented in handlers and UI deserializes safely
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/IngestionStepStatus.cslow
Atomic value object for step-execution status (Pending/Running/Succeeded/Failed/Skipped); mirrors aggregate status but per-step.
  • Line 33: Single() will throw if a step row has a stale/unknown StatusCode; verify EF config ensures only valid codes are persisted
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/IngestionStepType.csmedium
Defines the 10 executable pipeline steps + 3 append-only marker codes; FromCode and Pipeline/Markers collections support step sequencing and idempotency checks.
  • Line 74-77: Verify Pipeline order matches the choreography's expected execution sequence (ParseRawPayload → ... → CreateInboundDelivery); any out-of-order codes will silently allow duplicate execution
  • Line 85-86: Single() on Concat of Pipeline+Markers; if a code is in both lists or in neither, silently fails—audit the code allocation to prevent collisions
2

Domain — business rules

5 files1 high

Focus: The stop / start / resume / restart / delete transition guards. Verify each encodes the right allowed states and is evaluated before any mutation.

src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/Rules/IngestionCanBeDeletedOnlyWhenInactiveRule.csmedium
Guards delete operation: only Failed or Stopped statuses allowed; prevents orphaning active background jobs.
  • Verify rule correctly rejects Queued/Running (active jobs still reference this row; deletion risks job not found errors or orphaned work) and Succeeded (audit trail of real delivery)
  • Confirm InGracePeriodCode state: if an ingestion is paused in grace window, is status Stopped or InGracePeriod? If it's a separate status, rule may need to allow it
  • Check that callers enforce stop-first on any Queued/Running ingestion in UI/controller, preventing confusion when delete is rejected
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/Rules/IngestionCanBeRestartedUnlessCompletedRule.cshigh
Guards restart operation: blocks Succeeded and IboGenerated (delivery already created), allows Queued/Running/Failed/Stopped.
  • Verify IboGenerated constant is defined and documented as 'delivery created, awaiting generated order'; confirm it is the final pre-terminal state
  • Confirm attempt number is bumped atomically in the same transaction that checks this rule, ensuring stale background jobs see the new attempt and become no-ops
  • Ensure optimistic concurrency token is captured before restart begins; any in-flight converge job with old token will fail on SaveAsync() after restart commits
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/Rules/IngestionCanBeResumedOnlyFromGracePauseRule.csmedium
Guards resume (grace-window re-arm) operation: status must be Stopped AND reachedGracePhase flag must be true.
  • Critical: `IsBroken()` uses OR (||) to combine two conditions — verify operator precedence: `statusCode is not StoppedCode || !reachedGracePhase` reads as `(status ≠ Stopped) OR (grace not reached)`, correct logic
  • Confirm reachedGracePhase tracks whether UploadKeys step completed (marking entry into grace period); this flag must be immutable or only set once
  • Verify callers always pass the live flag value from the aggregate, not a stale copy; race with concurrent Start/Restart that clears the flag would bypass the rule
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/Rules/IngestionCanBeStartedOnlyWhenStoppedOrFailedRule.cslow
Guards start (resume-from-checkpoint) operation: only Stopped or Failed statuses allowed.
  • Verify rule blocks Queued/Running (already in-flight, no-op start rejected) and Succeeded (terminal, irreversible)
  • Confirm StoppedCode and FailedCode constants exist and are mutually distinct from QueuedCode/RunningCode
  • Ensure caller checks this rule BEFORE attempting to restore artifacts or bump checkpoint counters
src/WOCK.WholeSale.Domain/Delivery/InboundDeliveryIngestion/Rules/IngestionCanBeStoppedOnlyWhileActiveRule.cslow
Guards stop/pause operation: only Queued, Running, or InGracePeriod statuses allowed.
  • Verify `IsBroken()` logic is correct: should return true iff status is NOT one of the three allowed states (double-negative is correct here)
  • Confirm InGracePeriodCode constant is defined and matches the status enum used elsewhere in the ingestion domain
  • Check that callers invoke this rule at the start of the stop/pause handler (before any mutations), not after partial state changes
3

Shared delivery & key changes

4 files1 high

Focus: Edits to the existing InboundDelivery / Key so the converge can create a delivery with keys already uploaded. Review only these hunks — the skip-key-upload gate and pre-computed blob naming.

src/WOCK.WholeSale.Application/Deliveries/InboundDelivery/EventHandlers/InboundDeliveryCreated/UploadKeysToStorageEventHandler.csmedium
Internal event handler for synchronous key blob upload — gate at line 18 prevents double upload when async ingestion pre-uploads.
  • Verify early return at line 18–22 when SkipKeyUpload=true — ensures handler quits before entering AllFileKeys iteration (line 36) and upload batch loop (lines 33–64), preventing re-upload of already-persisted blobs
  • Check batch-upload batch size (line 34 = 5000) and BlobFileName resolution (line 43) — uses Key.BlobFileName which now calls BuildBlobFileName, so naming matches async pipeline's pre-upload calculation
  • Confirm Checksum validation (lines 46–49) throws if uploaded checksum ≠ key.Checksum — safe since async path pre-assigns both Id and Checksum, so this never hits when SkipKeyUpload=true
src/WOCK.WholeSale.Domain/Delivery/InboundDelivery/Events/InboundDeliveryCreated/InboundDeliveryCreatedEvent.cslow
Event factory wiring for skipKeyUpload flag — threads it through top-level event into InternalEvent so handler can conditionally skip blob upload.
  • Verify skipKeyUpload parameter propagates from InboundDeliveryCreatedEvent to InboundDeliveryCreatedInternalEvent (line 12) — false path (default) hits normal upload, true path (async ingestion) skips it
  • Check event defaults skipKeyUpload=false (line 10) so all existing callers remain safe without code change (backward compat for non-ingestion flows)
  • Confirm InboundDeliveryCreatedInternalEvent property getter (line 37) is public so handler can access; comment clarifies 'out-of-band upload' terminology
src/WOCK.WholeSale.Domain/Delivery/InboundDelivery/InboundDelivery.csmedium
Aggregate create factory for async ingestion path with conditional key upload — adds skipKeyUpload parameter to suppress in-process blob upload when keys are pre-uploaded.
  • Verify skipKeyUpload propagates to InboundDeliveryCreatedEvent constructor (line 163) — controls dual-upload gate at event-handler level
  • Confirm gracePeriod=0 at converge (line 108 CreateInboundDeliveryStepHandler) gates the grace-phase delay for generated orders; no status/timeline risk since event type is Delayed, not Internal
  • Check no state mutation of keys happens during Create — all key props (Checksum, Id) are pre-assigned by ingestion pipeline so Checksum validation in UploadKeysToStorageEventHandler line 46 always passes when keys exist
src/WOCK.WholeSale.Application/Deliveries/InboundDelivery/EventHandlers/InboundDeliveryCreated/SupplyInboundOrdersEventHandler.csmedium
Shared IBD-created handler that links/generates the Inbound Order from overflow; now also records the generated IBO on the scoped sink for the ingestion converge.
  • Verify sink.Record is additive — must NOT change behaviour for the sync / acquisition / resell paths (which never read the sink).
  • Confirm the IBO is created in the same transaction as the delivery (SaveChangesAsync on the Orders context).
src/WOCK.WholeSale.Domain/Delivery/Key.cshigh
Extracted static factory for blob naming + new CreateFileKey overload for async ingestion to fix KeyId before upload step.
  • Verify BuildBlobFileName is called by instance property (line 51) to ensure single source of truth for {Id}.{ext} naming — critical because async pipeline builds blob names from prepared-keys artifact (before DB row exists) and must match what app creates post-insertion
  • Check CreateFileKey(File, Guid, string) always sets IsNewest=true (line 162) — correct for new keys; resell logic sets OriginKeyId separately, so IsNewest remains true in 'new' key copy, false in old key (per docs lines 95–97)
  • Confirm Checksum parameter in overload accepts pre-computed value to skip MD5 recompute — critical for async pipeline that names blobs before DB persistence
4

Ports — abstractions & repository

7 files4 high

Focus: The interfaces the pipeline depends on. Check the contracts (and the required UserId on background commands) before the implementations.

src/WOCK.Framework.Persistence.Abstraction/Repositories/IInboundDeliveryIngestionsRepository.cshigh
Repository contract for loading ingestion aggregates with step-execution history, enabling step handlers and progress queries.
  • GetByGeneratedInboundOrderIdAsync: verify the reverse-lookup uniqueness constraint (one ingestion per order ID) is enforced in schema/indices.
  • Confirm GetWithStepsAsync loads eager-fetched steps collection; missing .Include(s=>s.Steps) in impl causes phantom step loss mid-pipeline.
  • GetInProgressWithStepsAsync filtering logic: check state enum covers all non-terminal states (Queued/Running/Failed/Stopped but not Completed/Canceled).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IInboundDeliveryIngestionStorage.cshigh
PVC I/O abstraction for raw multipart bodies (versioned per attempt), JSON artifacts, and temp file cleanup.
  • Stream methods (OpenRawPayload, OpenLatestRawPayload) return untracked Streams—verify callers dispose them; leaked streams block file deletion.
  • SaveNextRawPayloadAsync: version sequencing under retry—confirm idempotent re-save of same body returns same version key, not an incremented one.
  • PurgeOrphanTempFiles: cutoff logic must exclude files updated during active ingestions (race on touching files mid-parse); confirm timestamp granularity (second-level precision issues).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IGeneratedInboundOrderSink.cslow
Scoped one-shot hand-off for the IBO generated off a delivery's overflow keys — lets the converge read it back synchronously instead of a post-commit follow-up command.
  • Confirm it's registered scoped (via IService) so the converge and the SupplyInboundOrders it triggers share the instance and nothing leaks across requests.
  • Verify only the converge reads it; other Create paths ignore it.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IInboundDeliveryMultipartParser.csmedium
Parses stored raw multipart body into CreateInboundDeliveryCommand; writes extracted files to PVC; defers ZIP expansion.
  • ParsedUploadResult LineZipPasswords keyed by index—confirm indices are stable across multipart rebuild (concurrent parse retries must not reorder lines).
  • Parser writes files to PVC during parsing; if parser fails post-write, leftover temp files leak until cron cleanup. Verify error handling cleans files or marks attempt for manual review.
  • Stream disposal: confirm parser closes/disposes the input stream after consuming it (not caller's burden).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IInboundDeliveryZipExpander.csmedium
Unpacks ZIP archives into base-line artifact structure, appending extracted files as temp references; handles encrypted entries with passwords.
  • Expand(baseLine, zipTempFileName, password): mutates baseLine in-place then deletes archive file. If mutation fails mid-loop, artifact is partially modified with no rollback; verify caller persists artifact only on success.
  • WrongPassword flag: confirm it's set only when decryption fails (not on corrupt archive). UI must distinguish user-fixable (retry with password) from non-fixable (corruption).
  • Temp file deletion: confirm idempotency—if Expand throws after deleting archive, retry must not fail on missing file.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IIngestionKeyReservationService.cshigh
TOCTOU guard: reserves key hashes across processes during GenerateKeys step, preventing concurrent duplicate-key ingestions.
  • TryReserveAsync: "re-claiming hashes this ingestion already owns is a no-op"—confirm implementation uses CAS or atomic compare-and-set; optimistic locking on TTL expiry risks false conflicts.
  • TTL auto-expiry: if Redis TTL fires between reserve and release, later Release() silently succeeds on missing keys. Verify downstream converge step doesn't assume keys are still reserved when writing to DB.
  • Returned conflicts: document whether returned set is: (a) all keys currently held, (b) only keys held by OTHER ingestions, or (c) conflicting keys only. Ambiguous return type breaks retry logic.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IIngestionProgressService.cshigh
Hybrid read model: pushes progress snapshots to Redis cache + in-progress index; broadcasts state changes over SignalR; manages stop-requested flag.
  • PublishRunningAsync: synthetic running step row not persisted to DB. Verify aggregates loaded from DB don't carry stale cached 'running' state across command boundaries (cache vs. DB skew on restart).
  • Stop-signal lifecycle: RequestStopAsync sets flag, IsStopRequestedAsync checks it, ClearStopRequestAsync removes it. Race: step A checks flag (false), user calls RequestStop, step A runs anyway—confirm flag is re-checked immediately before critical operation.
  • RemoveAsync on delete: confirm SignalR broadcast uses correct ingestion ID and all clients unsubscribe from the same topic (no orphaned subscriptions from retransmits).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Abstractions/IKeysStorageMirror.csmedium
Mirrors uploaded file keys to dedicated keys-storage PVC volume 1:1 with blob storage; idempotent under Hangfire retry.
  • IsConfigured: confirm no-op skip is transparent to callers (exception vs. silent success path). If MirrorAsync is called when IsConfigured=false, does it throw or silently return?
  • Overwrite idempotency: "idempotent (overwrites)" for retry safety. Confirm file timestamp updates don't cause false freshness assumptions in downstream consumers (e.g., cron cleanup seeing newer file).
  • Parallel calls: if two Hangfire retries both call MirrorAsync(tempPath, same blobFileName) simultaneously, verify only one write persists (atomic rename or file lock).
5

Contracts — artifacts, results, responses

8 files2 high

Focus: The JSON artifacts passed step-to-step, the error catalogue, and the progress DTOs — the shapes everything else relies on.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/StepOutcome.cshigh
Step result enum-like type: Success vs Skipped (recorded in DB) vs NoOp (stale/duplicate, no DB record) vs BusinessFailure (error payload committed).
  • NoOp is distinct from Skipped to prevent idempotency poisoning — confirm handlers throw or return NoOp if detecting stale duplicate execution (e.g., step already executed in prior attempt or concurrent job)
  • IsBusinessFailure + ErrorPayload mutually exclusive with Skipped/Success — verify handler never creates BusinessFailure with null ErrorPayload or vice versa
  • Hangfire retry logic must treat Success/Skipped/BusinessFailure as terminal and NoOp as a signal to abort without poisoning the step record — confirm MediatRHangfireBridge respects this
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/InboundDeliveryUploadFields.cslow
Multipart form-data field-name contract; single source of truth linking request DTO properties to form field names (via nameof) so parser and re-expansion never drift.
  • All Base(line, property) calls must match actual form indices in parser — confirm line loop bounds prevent off-by-one in field name generation (e.g., if 3 lines, indices must be [0],[1],[2])
  • ZipPassword literal 'ZipPassword' has no DTO property — verify it is only used by unpacking step and never serialized back in form expansion (no round-trip)
  • nameof() bindings are compile-time checked — confirm if a request DTO property is renamed, this file must be manually updated (non-breaking from parser POV, but UI form breaks)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Models/ParsedPayloadArtifact.cslow
Per-attempt artifact carrying parsed request structure (partner, baselines with files and sublines); downstream steps hydrate domain objects from this.
  • ProductId nullable in base but non-nullable in prepared — verify null→int mapping during GenerateKeys doesn't lose line data
  • ZipPassword optional — confirm DecryptArchive step validates presence before attempting decompression or skips gracefully
  • TextKeys and Files share a line; confirm unpacking step enforces structural invariants (no orphaned files, folder depth rules respected)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Models/PreparedKeysArtifact.cshigh
Final prepared keys artifact (text + file with checksums + OriginKeyId) plus ExistingOriginKeyIdsToMarkNotNewest; reconstructed by Create step without re-running file factory.
  • ProductId is now non-nullable in PreparedBaseLineArtifact — assert parsing enforces presence or GenerateKeys rejects lines with no product
  • KeyId is generated in GenerateKeys and fixed thereafter (used by Upload step for blob naming) — verify Upload/Create steps use identical KeyId (no re-generation)
  • ExistingOriginKeyIdsToMarkNotNewest is a delivery-wide list; confirm DetectDuplicates step populates it correctly and Create step consumes it atomically per delivery
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Responses/InboundDeliveryIngestionProgressResponse.csmedium
Progress snapshot (shared by read queries and SignalR broadcasts) capturing ingestion state, current step, grace-period timers, and created delivery/order IDs; transient data appended for live updates.
  • OriginCode == 0 guard (line 100) — confirm this is the only un-migrated state and log/alert if hit in production (suggests stale row)
  • UpdatedAtUtc derived from steps.Max(FinishedAtUtc ?? StartedAtUtc) — verify this handles empty steps list (default) and partial-completion states correctly
  • GracePeriodStartedAtUtc + GracePeriodSeconds define pre-create window; DeliveryCompletionScheduledAtUtc + DeliveryGracePeriodSeconds define post-create window — confirm UI countdown logic never crosses these boundaries
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Responses/IngestionStepProgressDto.cslow
Per-step progress DTO (type, status, timestamps, error payload) mapped from domain IngestionStepExecution; returned in list on InboundDeliveryIngestionProgressResponse.
  • ErrorPayload is nullable string — confirm it is only populated when StepStatus is Failed (UI renders conditional on this; verify no orphaned error JSON)
  • FinishedAtUtc is nullable — confirm Skipped/Running steps have null and Succeeded/Failed have non-null (no edge case where both are populated)
  • Attempt field mirrors parent ingestion attempt — confirm multi-attempt retries re-populate full step list (not append-only) so UI sees stale attempt entries disappear
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Results/IngestionErrors.csmedium
Catalog of well-described, categorized error factories (code + category + title + message + explanation + subject) used by all pipeline steps for consistent error reporting.
  • Error codes are immutable strings used as idempotency keys by clients — verify no typos or future enum replacements that could break error lookups
  • KeyAlreadyInStock vs KeyAlreadyLoaded vs KeyNotDownloaded — confirm these three states are mutually exclusive and each mapped to exactly one scenario during DetectDuplicates
  • Archive errors (ArchiveUnreadable, ArchivePasswordProtected, ArchiveWrongPassword) — verify each is thrown from exactly one code path in UnpackArchives step (no silent fallback)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Results/IngestionStepFailure.csmedium
Result envelope (errors[] list + Summary) serialized to step's ErrorPayload (camelCase JSON); transient container during step execution, committed after step completes.
  • Add() returns this for fluent chaining — confirm all step handlers return IngestionStepFailure (not null) even on success (via IsFailed guard)
  • ToErrorPayload() uses CamelCasePropertyNamesContractResolver with NullValueHandling.Ignore — verify Summary=null on success produces '{summary:null,errors:[]}' and UI can deserialize
  • BuildSummary() groups by Category and formats as 'N problems found (A Ă— CatA, B Ă— CatB)' — confirm localization/i18n is not needed (message is admin-facing)
6

Commands & background-command base

22 files8 high

Focus: Command DTOs (carriers of IngestionId / Attempt / UserId) and the background-command base that restores user context. Check the attempt + user threading.

src/WOCK.WholeSale.Application/BackgroundCommandHandler.cslow
Generic base for background command handlers, restores acting user context on Hangfire job execution before work runs
  • User context restoration happens via backgroundService.SetUserContext before HandleAsync — matches IntegrationEventHandler pattern at line 22
  • Transaction wrapping is delegated to pipeline behavior (TransactionVoidCommandBehavior) — comment at line 14 correctly notes this, avoiding duplicate scope
  • IBackgroundCommand contract enforcement in generic constraint at line 18 — requires UserId property for user context
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/AwaitGracePeriodStepCommand.csmedium
Step 9: parks ingestion in grace window (pausable state) and schedules delayed converge; allows Stop/Resume.
  • Verify handler transitions status to Grace and records grace start time + token
  • Confirm delayed converge job uses same IngestionId + Attempt but includes GraceToken
  • Check that grace window duration is enforced; verify Resume re-schedules converge with same delay
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/CreateInboundDeliveryIngestionCommand.csmedium
Synchronous command fired by upload controller; creates initial aggregate and enqueues first step (ParseRawPayload).
  • Confirm handler reads UserId from IUserProvider and injects it into first enqueued step
  • Verify IngestionId is already generated by controller before sending; check idempotency if re-fired
  • Ensure RawPayloadVersionKey (optional) is passed through to converge step's blob operations
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/CreateInboundDeliveryStepCommand.cshigh
Step 10 (converge): creates delivery + generated order, uploads keys to blob, marks ingestion Succeeded.
  • Verify GraceToken check — handler no-ops if token mismatches (stale job from superseded grace window)
  • Confirm delivery creation is atomic with key blob upload (900s timeout in one transaction)
  • Check overflow line handling: SupplyInboundOrders generates the Inbound Order for overflow keys; the converge records it inline (scoped sink, → IBO generated) — no post-commit command
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/CreateInboundDeliveryStepCommandTimeout.csmedium
Transaction timeout override: grants 900s (15 min) for converge step to handle large deliveries.
  • Verify timeout is registered in DI and picked up by TransactionCommandBehavior
  • Check that 900s is sufficient for largest expected delivery (keys blob upload + DB writes)
  • Confirm timeout only applies to CreateInboundDeliveryStepCommand, not other steps
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/DeleteInboundDeliveryIngestionCommand.cslow
User-triggered or admin: permanently removes failed/stopped/superseded ingestion + stored payload.
  • Verify handler checks status (only allows delete if Failed/Stopped, not in-flight)
  • Confirm handler calls purge to delete raw payload + artifacts (orchestrates cleanup)
  • Check idempotency: deleting already-deleted ingestion (or non-existent) gracefully succeeds or clear error
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/DetectDuplicatesStepCommand.cshigh
Step 7: checks generated keys against existing deliveries + reserved keys to catch accidental re-uploads.
  • Verify check queries both Delivery (committed keys) and reservation table (in-flight ingestions)
  • Confirm attempt guard prevents re-checking after Reserve (idempotent read)
  • Check error handling: partial duplicates (some keys exist, some don't) — transition to Failed with reason
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/ExpandArchivesStepCommand.cshigh
Step 2: unpacks ZIP files into line+dependent structure; validates archive signatures.
  • Verify handler rejects malformed/corrupted ZIPs before mutating aggregate (idempotency safety)
  • Confirm ZIP expansion is deterministic (file order stable); check for path traversal/symlink attacks
  • Check that partial/incomplete expansions trigger failure (rollback) not intermediate state
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/GenerateKeysStepCommand.cshigh
Step 5: generates unique keys for each line; handles overflow lines (excess stock).
  • Verify key generation uses cryptographically secure random (UUID/GUID); check for collisions/reuse
  • Confirm overflow lines are separated from regular lines; check partition count vs max delivery capacity
  • Check attempt guard: if resurrected after attempt increment, verify keys are regenerated (idempotent format)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/InboundDeliveryIngestionStepCommand.cshigh
Abstract base for all 11 pipeline step commands; enforces IngestionId + Attempt + UserId (required) for stale-job filtering.
  • Verify Attempt field is used by every step handler to guard against superseded (retried/resumed) jobs executing twice
  • Confirm UserId is required at compile time and cannot be null/empty in any step command construction
  • Check that step handlers compare Attempt against aggregate's current attempt before proceeding
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/MarkIngestionTerminallyFailedCommand.csmedium
Enqueued by Hangfire terminal-failure filter when a step exhausts retries; transitions aggregate to Failed.
  • Verify Attempt field blocks stale failure (job from superseded attempt is ignored)
  • Confirm handler checks current status before transitioning (no double-transition if already Failed)
  • Check that Reason string is captured on aggregate for UI display; verify length limit or truncation
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/ParseRawPayloadStepCommand.csmedium
Step 1: parses stored raw payload into line structure (structure of the delivery).
  • Verify handler checks Attempt matches aggregate.Attempt before mutating state
  • Confirm parser preserves line order and validates file boundaries (no truncation/corruption)
  • Check that parse failure transitions aggregate to Failed (not left Queued); verify error classification
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/PurgeIngestionRawPayloadCommand.cslow
Fire-and-forget cleanup: deletes ingestion raw body + expand artifacts after successful converge.
  • Verify handler is scheduled post-converge-commit (blob delete cannot roll back failed converge)
  • Confirm purge is idempotent (deleting already-missing files does not error)
  • Check that purge failure (blob delete timeout) is logged but does not fail ingestion (fire-and-forget semantics)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/ReserveKeysStepCommand.cshigh
Step 6: atomically reserves all generated keys so concurrent ingestions cannot claim the same keys.
  • Verify reservation uses database row lock (SELECT FOR UPDATE or equivalent) inside transaction
  • Check that concurrent reserve attempts serialize (no race condition on same key set)
  • Confirm attempt guard blocks stale reservations; verify rollback cleans reservation on failure
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/ResolveAndValidateProductsStepCommand.cshigh
Step 4: resolves SKUs to products; validates SKU format, existence, and product state.
  • Confirm handler checks all SKUs in parsed lines (no skip); verify failure list + reason captured
  • Check that invalid SKU format blocks progression; verify product deleted/inactive blocks with clear error
  • Ensure product lookups are transactional (no read-committed ghosts); verify attempt guard
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/RestartInboundDeliveryIngestionCommand.csmedium
User-triggered: restarts failed ingestion with corrected raw payload (new version uploaded by controller).
  • Verify handler checks aggregate status (fails if not Failed)
  • Confirm RawPayloadVersionKey is updated; Attempt is incremented (supersedes previous attempt)
  • Check that PartnerId override (if supplied) is applied before re-enqueueing; verify validation
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/ResumeInboundDeliveryIngestionCommand.csmedium
User-triggered: resumes an ingestion paused during grace window; re-schedules delayed converge.
  • Verify status guard: only valid when status is Grace + StoppedDuring == Grace
  • Confirm resume reuses same Attempt + GraceToken (do NOT regenerate keys)
  • Check that converge delay is recalculated from Resume time (not original grace start)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/StartInboundDeliveryIngestionCommand.csmedium
User-triggered: restarts a Failed/Stopped ingestion from step 1, reusing stored raw payload.
  • Verify handler checks aggregate status (fails if not Failed or Stopped)
  • Confirm handler increments Attempt (so stale jobs from previous attempt are ignored)
  • Check that first enqueued step gets new Attempt value + UserId from IUserProvider (not from command)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/StopInboundDeliveryIngestionCommand.csmedium
User-triggered: pauses queued/running ingestion; only valid from Queued or Running status.
  • Verify status guard: blocks Stop if already Grace/Failed/Succeeded
  • Confirm in-flight jobs are NOT cancelled (stale-job guard via Attempt handles them)
  • Check that Stop during grace window records stop time + reason for Resume logic
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/UploadKeysStepCommand.cshigh
Step 8: uploads generated+reserved keys to blob storage and PVC before converge; pre-staging for delivery creation.
  • Verify upload is idempotent (re-uploading same ingestion reuses same blob path, no duplicates)
  • Check that partial blob upload failure (some files written, some not) is detected; verify rollback
  • Confirm blob transaction scope matches EF transaction (both commit or both rollback)
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Commands/ValidatePartnerStepCommand.csmedium
Step 3: verifies partner exists and is active; validates against partner rules.
  • Confirm handler fetches partner from repository inside transaction scope (no stale reads)
  • Verify partner inactive/deleted state blocks progression (transition to Failed)
  • Check that partner lookups use attempt guard to avoid re-validating after Resume
7

Base lifecycle & entry handler

2 files1 high

Focus: The shared step-handler lifecycle (stale/stop/idempotency guards, record-step, enqueue-next, transaction) and the HTTP entry that queues the ingestion. The highest-leverage files in the PR.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/CreateInboundDeliveryIngestionCommandHandler.csmedium
Entry handler: creates and persists the ingestion aggregate root in an initial state, priming the first step to be enqueued by the caller.
  • Duplicate IngestionId idempotency (line 22): confirm that repository.Add() + SaveChangesAsync() on a duplicate ID raises a DB constraint (not silent overwrite)—verify caller guards against double-submission of the same IngestionId within a race window.
  • Transaction scope (line 24): confirm SaveChangesAsync() inherits the outer command behavior's TransactionScope (IsolationLevel.ReadCommitted, 300s timeout)—if not, uncommitted aggregate could be visible to first step before full creation.
  • Hardcoded IngestionOrigin.WholeSale (line 20): confirm this is intentional (not a placeholder)—if other origins are expected, this will silently assign the wrong origin.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/InboundDeliveryIngestionStepHandler.cshigh
Abstract base orchestrating per-step lifecycle: load aggregate, guard against stale attempts & stops, execute domain work, classify exception as business (no retry) vs transient (retry), persist outcome, and enqueue next step.
  • Attempt version check (line 49): confirm that if an older step retries after a newer attempt's first step crashes (before committing its row), the older step is not mistakenly short-circuited—does HasStepCompleted(StepTypeCode, oldAttempt) return false if no row for newer attempt exists?
  • Stop-request flag atomicity (lines 147–158): ensure cache clear (line 154) and DB Stopped write (line 152) are serialized as one unit—verify no race between a retry observing the old flag and a concurrent stop-handler clearing it, leaving both steps thinking they own the halt.
  • Business-failure exception classification (line 199): confirm all exception types thrown by concrete step handlers are explicitly handled in IsBusinessFailure()—if a new exception escapes unclassified, it's treated as transient (retry loop risk).
8

Pipeline step handlers · 1–5 (parse → generate keys)

5 files2 high

Focus: Read each ExecuteAsync: artifact in/out, the business-failure classification, and the warehouse/key checks in Generate keys.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/ExpandArchivesStepHandler.csmedium
Step 2: Expand ZIP archives, mutate baseLine.Files list in-memory, rewrite artifact; skip if no archives (return Skipped status).
  • List mutation during enumeration: lines 54, 58—snapshot archives then Remove from baseLine.Files during foreach loop; verify modification doesn't corrupt underlying list or skip entries.
  • Archive corruption handling: lines 63-76 branch on ReadError vs HasUsableEntries; does WrongPassword + empty archive collision get tested? Edge case: archive with read error AND encrypted entries.
  • Artifact missing: line 46 throws InvalidOperationException if ParsedPayload artifact absent — in choreography, what triggers retry? Should this be a BusinessFailure instead to match step contract?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/GenerateKeysStepHandler.cshigh
Step 5 (heavy): Parse files, resolve text/file keys, run availability checks (existing keys, pick tower, not-downloaded, duplicates), emit prepared-keys artifact. Read-only to delivery data.
  • Duplicate detection logic (lines 197-204, 267-274): when allowDuplicates=false, entire request fails if ANY key is a dupe; when true, originIdsToMark collects duplicates for later 'mark not newest'. Test case: 10 keys, 1st is dupe—does request abort or proceed marking only the 1st?
  • Text-key vs file-key asymmetry: TextKeys path (line 173-214) vs FileKeys path (line 217-293) have divergent error-aggregation logic. Line 175 filters 'notDownloaded' as 'existing.Where(key => key.IsNotDownloaded && key.IsDuplicate)'—why both conditions? FileKeys line 245 only checks IsDuplicate. Root cause: can a text key be NotDownloaded but not a duplicate?
  • Pick-tower check short-circuit (lines 177-185, 247-254): if onPickTower.Any(), returns empty list immediately—does this hide subsequent validation errors (e.g., corrupted files) or is that the intent?
  • Dependent-line count mismatch (line 115-117): compares depKeys.Count vs baseKeys.Count, but baseLineKeyCount (line 103) is only file+text on base line—does not include dependent sub-lines. Is the comparison correct? Likely bug: should compare depKeys.Count vs baseLineKeyCount only if no files/text keys on dependent.
  • Progress reporting under delay (lines 135-138): paceMs calculation 'Math.Max(totalLines, 1)' will never hit the 1 case—OK, but verify delay doesn't cumulate into timeout.
  • File parsing early exit (line 224): fileFactory.CreateFiles() can append multiple results per file—verify loop doesn't double-process same file on retry.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/ParseRawPayloadStepHandler.cshigh
Step 1: Parse multipart body, validate it, persist parsed-payload artifact and set partner/duplicate flags on ingestion aggregate.
  • Artifact deserialization-time exceptions: does ParseAsync fail cleanly if raw body is malformed (not just validation via CreateInboundDeliveryCommandValidator)? Verify no silent truncation of multipart streams.
  • Idempotency: if re-run at same attempt after partial success, does re-parsing overwrite artifact or detect dupe? Check artifact load path doesn't race SaveArtifactAsync.
  • Partner ID mutation: ApplyParsedHeader() modifies ingestion aggregate outside transaction scope — verify no concurrent writes to PartnerId/AllowDuplicates if step retries fire during grace window.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/ResolveAndValidateProductsStepHandler.csmedium
Step 4: Load parsed artifact, check line count + note length, fetch products, validate state (not disabled/hold), flag unprocessable lines.
  • Sub-line processability: lines 80, 92-93 re-check IsProcessable() predicate; verify it's identical across both Step 4 and Step 5 (GenerateKeys also checks line 80). Any drift = inconsistent error reporting.
  • Product fetch: line 60 GetAllAsync(predicate) loads all products matching IDs; if a product is deleted between Step 3 and Step 4, does error reporting distinguish 'product not found' vs 'disabled'? Lines 97-111 scan array repeatedly (O(n²))—performance OK for MaximumDeliveryLines?
  • Off-by-one on line count: line 48 uses .Count (not .Count()?) — verify artifact.BaseLines is a List<> so Count is cached, not re-evaluated per check.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/ValidatePartnerStepHandler.csmedium
Step 3: Query partner repository; business-fail if PartnerId null or partner doesn't exist.
  • Race: PartnerId set by Step 1, queried here; if Step 1 and 3 overlap during retries, is PartnerId guaranteed written before this reads? Check if ingestion aggregate reload from DB reflects Step 1's ApplyParsedHeader().
  • Null check on Partner.Id: line 35 checks 'ingestion.PartnerId is null', but what if PartnerId is 0 or invalid? Does Domain allow PartnerId as nullable int?
9

Pipeline step handlers · 6–10 (reserve → converge)

5 files5 high

Focus: The concurrency-critical stretch: Redis reservation, in-load dedupe + release, blob upload, the grace schedule, and the token-guarded converge.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/AwaitGracePeriodStepHandler.cshigh
Step 9: Transition to grace-period state, schedule converge after N seconds, allow user Stop/Resume; returns null (no immediate next step).
  • GraceToken staleization: line 43 calls BeginGracePeriod(), then line 48–56 schedule converge with that token. If BeginGracePeriod() mutates ingestion state but SaveChanges fails, the orphaned scheduled job fires and CreateInboundDelivery checks token (line 61) — confirm token guard prevents spurious delivery creation.
  • Schedule-after-mutation order (line 45–47): Schedule() is NOT transactional. If commit rolls back, converge job fires but NoOp guard (status + token check at CreateInboundDelivery line 61) silently ignores it. Is this the intended idempotency model? Confirm no side effects of orphaned converge jobs.
  • Grace period = 0 edge case: timers.InboundDeliveryGracePeriod could be 0—does TimeSpan.FromSeconds(0) schedule immediately? Verify no races if user immediately Resumes before grace expires.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/CreateInboundDeliveryStepHandler.cshigh
Step 10 (converge): Reconstruct keys/lines from artifact, mark old keys not-newest, create InboundDelivery + generated Order, atomically with ingestion completion. Deletes temp files.
  • Idempotency guard (line 51–54): if CreatedInboundDeliveryId already set, returns Success(). But on first real execute + retry after commit, this guard silently skips recreation—is no-op acceptable or should it throw/error? Risk: if job fires twice before the first commit, second fires before guard takes effect, creating duplicate deliveries.
  • Grace-token staleization (line 61): status + token guard returns NoOp (NOT Success). NoOp skips recording step row per docstring (line 58–60). But what if a stale job fires AFTER a valid one completes? Both would return NoOp → no record → no signal to caller. Is this safe?
  • Temp-file deletion (line 117–138): try-catch swallows all exceptions (line 136). If a temp file is locked (still uploading?), silently left behind. Risk: orphan sweep may not find it if path format changes. Confirm cleanup is best-effort and converge doesn't fail on orphan.
  • Mark-not-newest (line 93–97): called BEFORE delivery created. If this update fails and rolls back, no delivery exists—idempotent. But if it succeeds then delivery-create fails, the old keys are marked not-newest forever. Is this acceptable? Verify domain semantics.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/DetectDuplicatesStepHandler.cshigh
Step 7: Scan all prepared keys for intra-load duplicates (text/checksum match); release reservations on failure so other ingestions aren't blocked.
  • Release-on-failure is critical: ReleaseAsync() called only if `failure.IsFailed` (line 70). Verify failure.Add() is comprehensive—does IngestionErrors.DuplicateInLoad() catch all duplicate types? Risk: silent no-ops if neither text nor checksum duplicates detected.
  • Double-release risk: if this handler fails and retries, does ReleaseAsync() idempotently handle already-released keys? Or does a retry throw?
  • Edge case: AllKeys list construction (line 43–50) mirrors ReserveKeys logic exactly. If PreparedKeysArtifact is corrupted/missing mid-pipeline, both handlers throw InvalidOperationException—confirm exception choice is correct (not a business failure).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/ReserveKeysStepHandler.cshigh
Step 6: Atomically reserve all load keys via reservation service; closes race between concurrent GenerateKeys passes by blocking duplicate keys.
  • Does TryReserveAsync correctly atomically exclude keys reserved by concurrent ingestions? Verify conflict detection is total-order (not lost-update race).
  • Hash collision risk: text-key prefix 't:' vs file-key prefix 'f:' are distinct, but verify no collisions with `key.Checksum` (nullable File?). Edge case: does null checksum/FileName get handled?
  • No rollback logic: if reservation succeeds, handler returns Success but next step fails (e.g., DetectDuplicates)—are keys held until release via DetectDuplicates.ReleaseAsync()?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/UploadKeysStepHandler.cshigh
Step 8: Batch-upload file keys to blob storage + keys-storage PVC in parallel before converge; skip if text-only. Retries re-upload idempotently.
  • Temp-file retention for idempotency: docstring says 'deleted by converge step'—but UploadKeyAsync throws if checksum mismatches (line 106). On retry after transient blob failure, temp files must still exist. Confirm temp cleanup logic is in converge only, never here.
  • Parallel upload race (line 100): both Task.WhenAll await blobTask twice (line 100, then line 102 reads result). Does awaiting blobTask twice work in .NET? Should cache result: `var uploadedChecksum = await blobTask;` before line 100.
  • Batch size = 5000 (line 40): no backpressure or error aggregation per batch. If batch 2 fails mid-chunk, already-succeeded batches 1+ are orphaned—idempotency must handle partial uploads on retry.
10

Control handlers

8 files3 high

Focus: Stop / resume / start / restart / delete / capture / purge / terminal-fail. Verify cache-flag vs aggregate-write, attempt bumping, and reservation release.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/DeleteInboundDeliveryIngestionCommandHandler.cshigh
Permanent cascade delete: aggregate row + steps + cache + reservations + PVC files (idempotent — missing aggregate is OK)
  • Idempotency and ordering (line 26-36): aggregate null-check before validation — does skipping EnsureCanBeDeleted when aggregate is gone create a loophole to delete Running/Succeeded?
  • Cascade cleanup (lines 41-43): RemoveAsync, ReleaseAsync, DeleteAllAsync run in sequence — if DeleteAllAsync fails, cache + reservations already cleared, leaving PVC orphaned. Is that acceptable or should order be reversed?
  • Storage deletion idempotency: DeleteAllAsync called twice (once null-aggregate path, once normal path) — is it safe or can it throw?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/MarkIngestionTerminallyFailedCommandHandler.cshigh
Hangfire terminal-failure hook: mark ingestion Failed (idempotent on attempt mismatch or already terminal)
  • Attempt guard (line 25): if ingestion.AttemptNumber != command.Attempt, silently return — does this correctly orphan the terminal-failure for restarted attempt N+1?
  • Already-terminal guard (line 34): MarkTerminallyFailed checks StatusCode and no-ops if already Succeeded/IboGenerated/Failed/Stopped — is this correct or should Failed+Failed be rejected harder?
  • Reservation release (line 43): called unconditionally after MarkTerminallyFailed succeeds — does it release keys for the correct attempt or could it collide with a concurrent Restart?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/PurgeIngestionRawPayloadCommandHandler.cslow
Async cleanup: delete raw bodies + artifacts from PVC, mark aggregate purged (idempotent; missing aggregate is OK)
  • Ordering (line 18): DeleteAllAsync happens BEFORE GetWithStepsAsync — if DeleteAllAsync throws transient, next Hangfire retry deletes again (already gone) but then marks purged; is that safe or can delete throw fatal error?
  • Missing aggregate (line 20): if aggregate is gone, mark-purged is skipped — does the cron sweeper assume purged=true only when both storage is cleared AND aggregate row exists? Or is this a logic gap?
  • Concurrent Delete handler: if DeleteInboundDeliveryIngestionCommandHandler fires at same time, does it call DeleteAllAsync again while this is mid-delete? Two concurrent deletes on same storage?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/RestartInboundDeliveryIngestionCommandHandler.cshigh
Bump attempt and re-queue from beginning (orphans prior attempt's artifacts and prepared keys)
  • Attempt bump (line 36): RestartFromBeginning increments AttemptNumber — are in-flight jobs from attempt N-1 guaranteed to become no-ops when they see attempt N?
  • Concurrency token: does the aggregate-version bump on restart allow a concurrent converge-from-attempt-N-1 to commit first, leaving restart in a partial state?
  • Reservation cleanup: restart bumps attempt but old reservations are still tied to ingestion id — does TryReserveAsync re-claim on attempt N succeed or collision-detect old attempt N-1 hashes?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/ResumeInboundDeliveryIngestionCommandHandler.csmedium
Re-arm grace window on same attempt (clear stale stop flag, issue fresh grace token, schedule new converge)
  • Validation before side effects (line 42): EnsureCanBeResumed guards correctly before ClearStopRequestAsync — does domain rule prevent resume from non-Stopped state?
  • Grace token invalidation (line 51): ResumeGracePeriod regenerates token — does old pending converge job (pre-pause) receive old token and no-op correctly?
  • Attempt guard: ingestion.AttemptNumber stays same during resume — do stale Hangfire jobs from pre-pause fire and check it?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/StartInboundDeliveryIngestionCommandHandler.csmedium
Re-queue from Stopped/Failed on same attempt (must reject grace-paused to avoid scheduling collision)
  • Grace-paused guard (line 33): IsPausedInGracePeriod check gates the grace-only path — if user calls Start on grace-paused, is error clear enough and does Resume get called instead?
  • Cache stop flag cleared (line 40): is clear idempotent if called twice? Does downstream step handler check the flag immediately or is there a race?
  • Queued event dispatch (line 45): Start() publishes InboundDeliveryIngestionQueuedEvent — does existing in-flight converge job (from prior stop/resume) get superseded or blocked by the re-Queued event?
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CommandHandlers/StopInboundDeliveryIngestionCommandHandler.csmedium
Pause signal (cache-only stop flag during Running/Queued, or sole-writer Stopped on grace window)
  • Cache-only vs. grace-window branch (line 44): once in InGracePeriod, is ingestion sole writer? Confirm no step can run concurrently
  • Stale stop flag re-rejection (line 58): IsStopRequestedAsync called after validation — does overlap with concurrent stop from another user constitute a real bug or benign double-signal?
  • Reservation leak: line 69 comment says reservations held across pause, but what if pause+restart cycle repeats N times? Confirm TTL is adequate and restart doesn't re-reserve
11

Event handlers

3 files

Focus: Pipeline start on the queued integration event, success on the order-completed event, and the completed-delivery broadcast. Check idempotency and correlation.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/EventHandlers/BroadcastInboundDeliveryCompletedEventHandler.cslow
Internal-event handler that re-broadcasts a delivery-completed signal over SignalR so the tester / UI refreshes when the converge finishes.
  • Confirm it only broadcasts (no state mutation) and is safe to run on every delivery-completed event (idempotent).
  • Verify it tolerates deliveries created outside ingestion (don't assume an ingestion row exists).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/EventHandlers/StartPipelineWhenIngestionQueuedEventHandler.cslow
Kicks off async ingestion pipeline by enqueuing first step after transaction commit
  • Confirm IntegrationEventHandler base guarantees post-commit execution (comment asserts this)
  • Verify Attempt field copied from event without off-by-one error
  • Check Hangfire enqueue is fire-and-forget, does not block or throw if unavailable
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/EventHandlers/SucceedIngestionWhenGeneratedOrderCompletedEventHandler.csmedium
Marks ingestion succeeded when generated order completes (IboGenerated to Succeeded transition)
  • Null ingestion return at line 39-41 is idempotent but verify not silent data loss (order completed before ingestion ready)
  • GetByGeneratedInboundOrderIdAsync must use unique index to prevent non-deterministic result
  • Audit trail captures userId and cross-context transition correctly
12

Cron, queries & query handlers

9 files1 high

Focus: The raw-payload purge cron (must include IboGenerated) and the read-side queries/handlers.

src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/CronJobs/PurgeStaleIngestionRawPayloadsCronJob.csmedium
Daily cron sweeper that purges succeeded/IboGenerated ingestions older than retention days and orphaned temp files older than retention hours.
  • Line 55: `DateTime.UtcNow.AddDays(-retentionDays)` — verify all ingestion timestamps (Trace.Updated/Created) are guaranteed UTC, not local or mixed; off-by-one if cutoff is inclusive and cleanup runs at exactly midnight UTC.
  • Line 60–65: `GetAllAsync()` filters on status + RawPayloadPurged + timestamp, then enqueues command per row — verify no race where ingestion transitions to Succeeded between filter and enqueue, causing duplicate purge commands.
  • Line 69–74: `PurgeIngestionRawPayloadCommand` is enqueued without transaction — if an ingestion state changes before the command executes, command may attempt to purge an ingestion that should be retained or is already purged.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Query/GetCompletedInboundDeliveryIngestionsQuery.cslow
DTO container with pagination: queries most recently completed ingestions (succeeded), retained as audit trail with optional raw-payload download.
  • Line 13: Default `Take = 50` — reasonable, but verify AppSettings doesn't override this to an unsafe value (should cap at 200).
  • No timestamp ordering specified in query — confirm handler sorts by creation date descending (newest first) per docstring.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Query/GetExpandedInboundDeliveryIngestionPayloadQuery.cslow
DTO container: queries an ingestion's expanded payload (parsed artifact + extracted files rebuilt as multipart form), with null fallback to raw payload.
  • IngestionId is Guid, no validation — confirm handler null-checks and cascades to raw-payload logic correctly.
  • Docstring says 'returns null when expanded cannot be served completely' — verify handler distinguishes between missing artifact and missing extracted file (both should return null).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Query/GetInboundDeliveryIngestionQuery.cslow
Simple DTO container: queries a single ingestion by ID, returns InboundDeliveryIngestionProgressResponse.
  • IngestionId is Guid but no validation — verify calling handler will return null gracefully if ID is Guid.Empty or invalid format.
  • No pagination or filtering — acceptable for single-item query, but confirm client calls correctly and doesn't bulk-query via loops.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/Query/GetInProgressInboundDeliveryIngestionsQuery.cslow
Simple DTO container: no parameters, queries all in-progress ingestions (Queued/Running/Failed) for warehouse list view.
  • No pagination — confirm repository returns bounded result set (check `GetInProgressWithStepsAsync` contract). Risk of OOM if thousands of concurrent ingestions.
  • No caching at query level — each request re-fetches all in-progress ingestions; can spike DB load during concurrent uploads.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/QueryHandlers/GetCompletedInboundDeliveryIngestionsQueryHandler.cslow
Fetch recent completed ingestions, clamp page size, map to response DTOs.
  • Line 20: `Math.Clamp(request.Take, 1, 200)` silently changes input — confirm no downstream validation fails (e.g., API caller expects exact Take count) or client logging tracks the clamp.
  • Line 22: `GetRecentlyCompletedWithStepsAsync(take)` — verify repository enforces DESC creation order (newest first), not asc.
  • No caching — each call re-queries DB; fine for audit queries, but confirm not called in a tight loop by UI.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/QueryHandlers/GetExpandedInboundDeliveryIngestionPayloadQueryHandler.cshigh
Rebuild ingestion's expanded payload: load parsed artifact from storage, then stream extracted files from PVC into a multipart/form-data body. Null if artifact or any file missing, falling back to raw upload.
  • Line 38–42: Null-check ingestion — but if ingestion exists + artifact deleted before handler runs, handler returns null at line 48 (correct fallback).
  • Line 44–45: `LoadArtifactAsync()` — if deserialization fails, exception propagates (not caught). Verify artifact schema is versioned and backward-compatible.
  • Line 67: `storage.ReadExtractedFile()` returns null if file missing — but at line 73 `.OriginalFileName ?? .TempFileName` assumes TempFileName exists; if both are null, NRE at line 74. Verify ParsedFileRefArtifact guarantees non-null TempFileName.
  • Race condition: extracted file deleted by cron job (PurgeOrphanTempFiles) between LoadArtifactAsync and WriteFiles loop — returns partial multipart + null at line 98–111. Acceptable fallback, but verify caller closes stream gracefully.
  • Line 91–93: Optional fields (ProductId, Note, ZipPassword) only written if non-null/non-empty — verify client parser handles missing fields in multipart (should, since original form might omit them too).
  • Line 51: Random boundary value — verify no collision risk (GUID suffix should be sufficient) and no boundary substring in file contents.
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/QueryHandlers/GetInboundDeliveryIngestionQueryHandler.csmedium
Fetch one ingestion: cache-first (via progressService), fallback to DB + map to progress DTO. Used by SignalR client and UI for real-time status.
  • Line 20–24: `TryGetAsync()` returns cached progress if present — verify cache key is IngestionId only (no attempt-number scope); stale if attempt changes mid-grace without cache invalidation.
  • Line 26: `GetWithStepsAsync()` fetches ingestion + all steps — verify no N+1 if steps collection is large (check EF query plan).
  • Line 28: Returns null if ingestion not found — correct, but verify API controller returns 404 (not 200 with null body).
src/WOCK.WholeSale.Application/Deliveries/InboundDeliveryIngestion/QueryHandlers/GetInProgressInboundDeliveryIngestionsQueryHandler.csmedium
Fetch all in-progress ingestions, then per-row check cache for stop-requested flag; used by warehouse list refresh.
  • Line 22: `GetInProgressWithStepsAsync()` fetches DB list once — verify repository filters on status (Queued/Running/Failed) and returns bounded result.
  • Line 29–34: Per-response, call `IsStopRequestedAsync()` (cache-only) for Queued/Running — if cache check is slow or blocking, scales O(N) with concurrent ingestions; potential to slow down list refreshes.
  • Race condition: ingestion transitions to Succeeded after line 22 fetch but before line 33 cache check — response will show status mismatch (e.g., Queued in DB but StopRequested=true in cache). Mirror the cache-check before returning.
13

Services & factories

5 files

Focus: The concrete implementations: PVC storage, ZIP expander, Redis key reservation (Lua), progress service, keys mirror.

src/WOCK.WholeSale.Factories/Ingestion/InboundDeliveryIngestionStorage.csmedium
PVC-backed ingestion artifact store with per-ingestion Redis lock for concurrent upload reservation; raw payloads and attempt-numbered artifacts.
  • Verify Redis lock timeout degrades safely: does optimistic concurrency token on the aggregate actually prevent duplicate attempts on Redis outage (not just minimize collision window)?
  • Check: if CopyToAsync() throws mid-body after folder is created, is the orphaned attempt-{n}/ folder cleaned up or does it block future attempts via NextAttempt() count?
src/WOCK.WholeSale.Factories/Ingestion/GeneratedInboundOrderSink.cslow
Trivial scoped holder implementing IGeneratedInboundOrderSink (IService → scoped).
  • Confirm it holds only the single recorded value and gets a fresh instance per command scope.
src/WOCK.WholeSale.Factories/Ingestion/InboundDeliveryZipExpander.cslow
Parses and expands ZIP archives into base-line + dependent sub-lines; separates encrypted entries, skips nesting >2 levels.
  • Confirm: each unique folder name generates a fresh RequestId (line 142). Is this intentional or should duplicate folder names reuse same sub-line?
  • Verify temp file cleanup on ExtractEntry() exception: if entry.Extract() fails but file is already created, is it left behind?
src/WOCK.WholeSale.Factories/Ingestion/IngestionKeyReservationService.cslow
Atomic Redis-backed key reservation via Lua script; guards against concurrent claims with owned-set-per-ingestion cleanup.
  • Verify Lua array indexing aligns with C# arg construction (lines 71-84): KEYS[0]=owned-set, KEYS[1..]=keys; ARGV[0]=ingestionId, ARGV[1]=ttl, ARGV[2..]=hashes. Script expects ARGV[1]=owner, ARGV[2]=ttl.
  • Confirm: if Distinct() (line 65) removes duplicate hash inputs, does the calling code expect this behavior or should duplicates error?
src/WOCK.WholeSale.Factories/Ingestion/IngestionProgressService.cslow
Cache-backed progress snapshot with SignalR broadcast; non-DB running state + stop flag; synthetic step injection for live UI.
  • Check: synthetic running step (lines 109-120) uses DateTime.UtcNow for StartedAtUtc. If PublishStepProgressAsync() is called multiple times per step, does this create multiple entries or update one?
  • Verify stop flag lifecycle: 6h TTL. If step takes 5.9h + 1m, does stop flag expire mid-execution? Is this acceptable or should TTL be step-duration aware?
src/WOCK.WholeSale.Factories/Ingestion/KeysStorageMirror.cslow
Optional secondary PVC mirror for blob file keys; no-op if not configured; plain file copy.
  • Verify: blob file name (blobFileName param) is sanitized before combining with prefix path (line 31)—is directory traversal (../) possible?
  • Confirm: if CopyToAsync() fails, does the ingestion fail or silently continue with partial mirror? Is this intended?
14

Framework & shared services

6 files

Focus: SignalR broadcast additions, cache interface, the new AppSettings ingestion fields, and the test mocks. Review the diffs only.

src/WOCK.Framework.Cache/ICacheService.cslow
Cache abstraction interface documentation only (no logic change)
  • Docstring on Remove() clarifies no-op semantics when key missing — defensive; matches caller expectations in IngestionProgressService.PublishStopRequestedAsync and RemoveAsync
  • Verifies cache callers (IngestionProgressService lines 74, 80, 86, 94, 97) don't assume exception on missing key
src/WOCK.Framework.Utils/Mocks/BackgroundServiceMock.cslow
Test infrastructure: record-only command queue for deterministic async pipeline execution
  • EnqueuedCommands Queue<IBackgroundCommand>: captures Enqueue() calls without firing; ClearEnqueuedCommands() resets between tests
  • Enqueue(IBackgroundCommand) override (line 52-55) matches base signature and queues synchronously — no dispatch race
  • Integration test pattern: drain queue via MediatR.Send(dequeued commands) after transaction commits, avoiding flaky async waits; docstring explains intent clearly
  • Test isolation: queue lives per test (cleared by ClearEnqueuedCommands), no cross-test leakage
src/WOCK.Framework.Utils/Mocks/NotificationServiceMock.cslow
Test infrastructure: record-and-verify broadcast notifications for assertion in tests
  • Broadcasts List<(string Method, object Payload)> captures all BroadcastToAll() calls; no-op in-memory storage
  • BroadcastToAll() (line 18-23) matches interface exactly; Task.CompletedTask allows await without blocking
  • Used by IngestionProgressServiceTests to verify progress updates publish correctly; no race conditions (list append is atomic per entry)
src/WOCK.Framework.WebAPI.Abstraction/Models/AppSettings.cslow
Central config model for ingestion limits, retention, and storage routing
  • 4 new int/string properties all default-initialized (0 or null): KeysStoragePrefix, IngestionStepDelaySeconds, IngestionRawPayloadRetentionDays, IngestionMaxUploadMegabytes; defaults are defensive (skip optional features)
  • KeysStoragePrefix: used by KeysStorageMirror.IsConfigured (check: !string.IsNullOrWhiteSpace) — correctly gated; no exception if unset
  • IngestionMaxUploadMegabytes: controller ValidateUploadSize() checks <=0 as no-limit signal — correct (line in InboundDeliveryIngestionsController: 'if (appSettings.IngestionMaxUploadMegabytes <= 0) return null')
  • IngestionRawPayloadRetentionDays: cron job PurgeStaleIngestionRawPayloadsCronJobHandler checks >0 before purging — safe default-retain-forever
  • IngestionOrphanFileRetentionHours: paired with RetentionDays, same >0 gating pattern
src/WOCK.WholeSale.Services/Websocket/NotificationService/INotificationService.cslow
Interface contract for bidirectional notification delivery (user-targeted + broadcast)
  • BroadcastToAll(string method, object payload) signature matches implementation (NotificationService.cs line 22-25) and all call sites (IngestionProgressService line 76, 98; BroadcastInboundDeliveryCompletedEventHandler line 22)
  • Async Task return type consistent with SendNotification; awaitable at call sites
  • Method documentation clarifies payload structure expectation for clients
src/WOCK.WholeSale.Services/Websocket/NotificationService/NotificationService.cslow
SignalR hub wrapper delegating to IHubContext<WholeSaleHub>.Clients.All.SendAsync
  • BroadcastToAll delegates directly to hub: _notificationHub.Clients.All.SendAsync(method, payload) — no serialization/filtering; payload is POCO
  • No null-checks on method or payload — assumes caller validates; matches pattern of SendNotification
  • Concurrency: SignalR's IHubContext is thread-safe (stateless), multiple concurrent broadcasts are safe
15

WebAPI & infrastructure

7 files3 high

Focus: The controller, the streaming multipart parser, the terminal-failure Hangfire filter, runtime config hot-reload, and the DI wiring in Startup. Confirm everything is registered.

src/WOCK.WholeSale.WebAPI/Controllers/Deliveries/InboundDeliveryIngestionsController.cshigh
HTTP API surface for async inbound-delivery ingestion: handles uploads, state transitions (stop/start/resume/restart/delete), and audit download endpoints (raw + expanded payloads).
  • DownloadPayload & DownloadExpandedPayload: verify the returned stream is properly disposed—File() method must guarantee cleanup on completion or error; if storage manager returns unclosed streams, add using(). Confirm no race condition between async query and file stream availability.
  • GetCompleted: take parameter is clamped to 50 if ≤0 but has no upper bound—add cap (e.g., take = Math.Min(take, 1000)) to prevent DoS.
  • Restart/Start/Resume/Delete: commands validate ingestion state (e.g., 'not in Failed state' for restart)—confirm exception flows back to client as 400 BadRequest, not 500; check if command handler throws DomainValidationException or custom exception.
src/WOCK.WholeSale.WebAPI/Infrastructure/Configuration/IngestionRuntimeConfigReloader.cslow
IHostedService that bridges hot-reloadable IOptionsMonitor onto shared AppSettings singleton, propagates ConfigMap changes without pod restart
  • OnChange handler at line 37 fires on ConfigMap reload — equality check at line 50 prevents spurious updates and logging on no-change reloads
  • Direct property assignment to AppSettings singleton at line 58 is atomic for int (IngestionStepDelaySeconds) — safe concurrent read by step handlers
  • Subscription cleanup at line 43 during StopAsync — prevents dangling reference when reloader is disposed
src/WOCK.WholeSale.WebAPI/Infrastructure/Configuration/IngestionRuntimeOptions.cslow
Configuration POCO for runtime-tunable ingestion settings, bound to AppSettings section with reloadOnChange enabled
  • Single property IngestionStepDelaySeconds at line 12 matches AppSettings property name — binding contract is stable
  • Class design supports expansion (comment line 7 says 'add more runtime-tunable fields here as needed') — extensible without pipeline changes
src/WOCK.WholeSale.WebAPI/Infrastructure/Filters/IngestionStepTerminalFailureFilter.cslow
Hangfire job filter: marks ingestion terminally failed when a pipeline step exhausts retries (transient/infra errors), prevents stuck Running state
  • RetryCount==MaxRetries (10) guard at line 33 ensures filter acts only on final permanent failure, not intermediate retries — prevents false positives
  • Extracts InboundDeliveryIngestionStepCommand via OfType at line 39 — safe null check at line 42 skips jobs that don't carry step context
  • Enqueue of MarkIngestionTerminallyFailedCommand at line 47 passes IngestionId+Attempt+UserId — allows handler to skip stale failures from superseded attempts
src/WOCK.WholeSale.WebAPI/Infrastructure/Ingestion/InboundDeliveryMultipartParser.csmedium
Streaming multipart form parser: deserializes stored raw body into structured command, writes uploaded files to PVC, leaves ZIPs for unpacking step
  • File stream lifecycle management: creation on partNumber==0 (line 158), reuse on subsequent chunks (line 168), proper DisposeAsync in loop (line 195) — prevents file handle leaks
  • Multipart field parsing uses InboundDeliveryUploadFields constants (lines 31–41) — single source of truth prevents silent drift with expanded-payload writer
  • File indexing via fileKey=[baseIndex,subIndex,fileIndex] tuple (line 153) correctly routes chunks to the right file and command structure (lines 174–188)
src/WOCK.WholeSale.WebAPI/Program.cshigh
Configuration loading setup with hot-reload support for Kubernetes ConfigMap mutations and polling file watcher.
  • Lines 25-34: DOTNET_USE_POLLING_FILE_WATCHER env var setup before host build - verify polling watcher is set only if unset (idempotent). Kubernetes ConfigMap symlink swaps require polling; FileSystemWatcher can miss them. Critical for reloadOnChange to work reliably.
  • Lines 65-74: All four appsettings.json sources now have reloadOnChange: true (shared + environment-specific + published + ConfigMap overrides). Verify order: shared base -> env-specific override -> published -> ConfigMap hot-reload. ConfigMap path must be mounted at {ContentRoot}/config/appsettings.overrides.json in Kubernetes manifests.
  • Lines 73-74: Optional ConfigMap overrides file + hot-reload - verify Kubernetes pod spec mounts ConfigMap as volume (not subPath) to preserve symlink behavior. If missing, file doesn't exist and optional: true allows startup. IngestionRuntimeConfigReloader bridges changes onto AppSettings singleton.
src/WOCK.WholeSale.WebAPI/Startup.cshigh
Dependency injection & service wiring for ingestion runtime, command timeouts, Hangfire filters, and conditional Azure SignalR.
  • Lines 133-139: IngestionRuntimeOptions config binding to AppSettings section + IngestionRuntimeConfigReloader hosted service - verify DI resolution chain works (IOptionsMonitor -> reloader -> AppSettings singleton). AppSettings must have IngestionStepDelaySeconds property (confirmed in Framework.WebAPI.Abstraction).
  • Line 208: ICommandTimeout<CreateInboundDeliveryStepCommand> registration - verify this timeout (900s) matches the step's load profile (converge writes delivery + uploads keys in one transaction). Mirrors line 207 for synchronous flow.
  • Lines 405-412: Azure SignalR conditional wiring - verify 'Azure:SignalR:ConnectionString' config section matches the Program.cs configuration loading order. Local dev without Azure MSI can now run without SignalR connection; prod/QA uses Azure service. No race condition since service builder is assigned before condition.
16

Persistence & EF config

3 files

Focus: The EF configurations and the repository. Check column types/lengths, the owned Trace, and the eager-load includes.

src/WOCK.WholeSale.Persistence/Deliveries/Configuration/InboundDeliveryIngestionConfiguration.cslow
Aggregate root EF config—Wires step child collection, ignores transients, applies audit trait.
  • Steps HasMany with Cascade delete (lines 17-20)—matches FK constraint in migration
  • Status/CurrentStep/Origin ignored (lines 25-27)—computed from *Code properties
  • DeliveryCompletionScheduledAtUtc/DeliveryGracePeriodSeconds ignored (lines 30-31)—transient broadcast-only fields
  • HasAudit() call (line 33) auto-configures Trace (OwnsOne) + Audits (OwnsMany) via TraceableConfiguration generic + HasAudit extension
src/WOCK.WholeSale.Persistence/Deliveries/Configuration/IngestionStepExecutionConfiguration.cslow
Child entity EF config—Step execution audit rows within ingestion aggregate.
  • ValueGeneratedNever on Id uses SequentialGuidGenerator per domain model (lines 12-13)
  • Index on (Attempt, StepTypeCode) wired correctly for step-retry lookups (line 15)
  • ErrorPayload HasMaxLength(int.MaxValue) matches nvarchar(max) schema (line 19)
  • StepType, StepStatus ignored correctly; no Trace/Audits (entity is not ITraceable, line 21-22)
src/WOCK.WholeSale.Persistence/Deliveries/Repositories/InboundDeliveryIngestionsRepository.csmedium
Data access layer for InboundDeliveryIngestion: eager-loads Steps and Trace.Creator; supports querying by id/order-id, filtering in-progress by status code, and paginating recently-completed.
  • GetInProgressWithStepsAsync: ensure IngestionStatus.InProgressCodes is never empty/null and contains valid status codes—add defensive assertion or unit test.
  • GetRecentlyCompletedWithStepsAsync: verify .Take() appears *after* all .Include() calls in the LINQ chain (current order chains includes after take, which may cause N+1).
  • Confirm all queries return fully-hydrated aggregates for business rule evaluation—Trace.Creator load ensures SignalR snapshots include author name without flicker.
17

EF migrations

9 files

Focus: Verify the migrations match the configs and are forward-only. Focus on the migration Up(); the *.Designer / snapshot files are EF-generated.

src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260530044305_AddedIngestion_Deliveries.csmedium
Initial schema creation—Three ingestion tables (main aggregate + audit + step child).
  • PK and three FKs to User(settings.User) all Restrict on delete; InboundDeliveryIngestionAudits→InboundDeliveryIngestion Cascade (lines 36-107)
  • IngestionStepExecution FK to InboundDeliveryIngestion Cascade ensures cleanup (line 102-107)
  • InboundDeliveryIngestion.StatusCode indexed (line 117-120); IngestionStepExecution (Attempt, StepTypeCode) indexed (line 141-144)
  • Trace columns (Created, CreatedBy, Updated, UpdatedBy) embedded in InboundDeliveryIngestion table per convention (lines 30-33); Audits table separate
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260530044305_AddedIngestion_Deliveries.Designer.cslow
EF-generated migration snapshot (Designer) — mirrors the model state at this migration; not hand-written.
  • Generated by EF; verify it regenerates cleanly and matches the entity configs — no line-by-line review of the body needed.
  • Confirm it pairs with its migration `.cs`.
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260611133321_IngestionSimplify_Deliveries.cslow
Simplification—Dropped ContentType and OriginUserId; replaced latter with OriginCode enum.
  • ContentType drop is safe (unused per domain comments)
  • OriginUserId drop is safe (OriginCode added in next migration); Down() restores defaults correctly
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260611133321_IngestionSimplify_Deliveries.Designer.cslow
EF-generated migration snapshot (Designer) — mirrors the model state at this migration; not hand-written.
  • Generated by EF; verify it regenerates cleanly and matches the entity configs — no line-by-line review of the body needed.
  • Confirm it pairs with its migration `.cs`.
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260616063223_IngestionAdjustments_Deliveries.cslow
Grace-period state persistence—Four new nullable columns for pause/resume logic.
  • GracePeriodSeconds, GracePeriodStartedAtUtc, GraceToken nullable (lines 14-33)
  • OriginCode NOT NULL with defaultValue: 0 (line 35-41)—safe, but verify domain code handles zero default
  • Down() drop/restore logic correct; no data loss on rollback
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260616063223_IngestionAdjustments_Deliveries.Designer.cslow
EF-generated migration snapshot (Designer) — mirrors the model state at this migration; not hand-written.
  • Generated by EF; verify it regenerates cleanly and matches the entity configs — no line-by-line review of the body needed.
  • Confirm it pairs with its migration `.cs`.
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260619071643_IngestionDeliveryAndOrderReferences_Deliveries.cslow
Post-ingestion reference capture—Added three columns to record generated delivery/order IDs.
  • CreatedInboundDeliveryReference, GeneratedInboundOrderId, GeneratedInboundOrderReference all nullable (lines 14-35)
  • All nvarchar(250) match domain string properties (line 19, 33)
  • Down() safely drops all three columns
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/20260619071643_IngestionDeliveryAndOrderReferences_Deliveries.Designer.cslow
EF-generated migration snapshot (Designer) — mirrors the model state at this migration; not hand-written.
  • Generated by EF; verify it regenerates cleanly and matches the entity configs — no line-by-line review of the body needed.
  • Confirm it pairs with its migration `.cs`.
src/WOCK.WholeSale.Persistence.Migrations/Deliveries/DeliveriesContextModelSnapshot.csmedium
Fluent API snapshot—Captures ingestion schema state after four migrations.
  • InboundDeliveryIngestion: Trace OwnsOne (FK to User CreatedBy/UpdatedBy both Restrict, Trace.IsRequired, line 2070-2099); Audits OwnsMany table InboundDeliveryIngestionAudits (FK CreatedBy Restrict, line 2101-2147)
  • IngestionStepExecution: No Trace/Audits; FK to InboundDeliveryIngestion Cascade (line 2183-2186)
  • Index on InboundDeliveryIngestion.StatusCode present (snapshot line 865); Index on IngestionStepExecution (Attempt, StepTypeCode) and InboundDeliveryIngestionId present (snapshot line 2187-2190)
18

Tests

20 files9 high

Focus: Coverage of the aggregate, rules, base lifecycle, each handler, and the integration test. Check the risky paths (grace token, stop/resume, converge) are actually asserted.

src/WOCK.WholeSale.Tests.Integration/Deliveries/Inbound/InboundDeliveryIngestionIntegrationTests.cshigh
End-to-end orchestration: happy-path delivery creation, per-step failure modes, concurrency guard, stop/start/resume/delete lifecycle, and query coverage.
  • Concurrent key reservation correctly blocks Reserve step and fails with right error code
  • Start/Stop/Resume commands execute expected state transitions; pipeline converges without loops (max 200 steps)
  • Grace phase (if tested) correctly schedules delayed converge and supports pause/resume without losing keys
  • Query endpoints (detail, list, completed, payload) all return 200 OK after successful ingestion
src/WOCK.WholeSale.Tests.Integration/Helpers/EndpointsPaths.cslow
Test routing constant for ingestions sub-endpoint
  • InboundDeliveryIngestionsPath = '/DeliveriesManagement/Inbounds/Ingestions' — mirrors InboundDeliveriesController routing; enables test clients to target upload endpoint
  • Constant string immutable; no cross-environment or secret concerns
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/AwaitGracePeriodStepHandlerTests.cshigh
Grace-window entry and converge delay-scheduling: grace token matching, scheduled vs. enqueued distinction, and proper step completion.
  • Grace token embedded in CreateInboundDeliveryStepCommand matches GraceToken on ingestion
  • Converge is scheduled (delayed), not enqueued (immediate) — verify no immediate next command
  • AwaitGracePeriod step is recorded as Succeeded, leaving ingestion in InGracePeriod status
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/CreateInboundDeliveryIngestionCommandHandlerTests.cslow
Initial ingestion creation: aggregate instantiation with WholeSale origin and baseline persistence.
  • Ingestion created with correct origin code (WholeSaleCode, not null/Acquisition)
  • Aggregate added to repo and SaveChangesAsync called exactly once
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/DetectDuplicatesStepHandlerTests.cshigh
In-load duplicate detection: same-key across lines, failure release of reservations, success chain advancement.
  • In-load duplicate (within or across lines) fails and releases reservation synchronously
  • Clean load keeps reservation held (does not release), allowing next step to upload keys for the held set
  • Failure does not advance chain; success advances to UploadKeys
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/ExpandArchivesStepHandlerTests.csmedium
Archive unpacking step: detection, successful expansion, corruption/encryption/password errors, and graceful skip on non-archive.
  • Empty archive (no usable files) fails with business error before advancing chain
  • Corrupt archive and wrong-password cases error-payload contains specific codes (archive_password_protected, archive_wrong_password)
  • Non-archive file skips gracefully without updating artifact storage, still chains to next step
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/GetExpandedInboundDeliveryIngestionPayloadQueryHandlerTests.cslow
Extracted-file multipart reconstruction: fallback to raw on missing files, dependency hierarchy nesting, and MIME attachment serialization.
  • Missing extracted file (gone from temp storage) returns null, triggering tool fallback to raw multipart
  • Multipart rebuilds partner/line/subline hierarchy with correct form-field names for API binding
  • File attachments carry original filename and MIME type
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/InboundDeliveryIngestionStepHandlerTests.cshigh
Step handler base contract: superseded-attempt drop, retry-on-newer-attempt, stop flag halting, skipped/success/business-failure outcomes, and idempotent fast-forward.
  • Superseded older attempt (attempt 1 after restart to 2) silently drops without touching DB — no orphaned step rows
  • Newer attempt not yet visible (READ_COMMITTED_SNAPSHOT) throws (not drops) so Hangfire retries — critical for restart first-step safety
  • NoOp outcome records zero step rows to avoid false short-circuit of live job; Transient exception leaves ingestion unchanged (no premature Failed flip)
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/InboundDeliveryIngestionTests.cshigh
Domain aggregate state machine: creation, step transitions, grace phase entry/pause/resume, terminal status guards, and step completion idempotency.
  • Grace-pause and resume use distinct tokens — verify re-enqueuing old pause does not double-execute the converge
  • RecordStepSucceeded on a terminal status never downgrades — check Succeeded post-converge records CreateInboundDelivery step without flip
  • ResumeGracePeriod rejects non-paused ingestions and maintains attempt number (no synthetic restart)
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/CreateInboundDeliveryStepHandlerTests.cslow
Unit tests for the converge's inline IBO recording: generated order → IBO generated; none → Succeeded.
  • Confirm both branches assert status + GeneratedInboundOrderId and that no follow-up command is enqueued.
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/IngestionProgressServiceTests.cslow
Stop-flag cache and progress broadcasting: cache lifecycle, synthetic running-step injection, and SignalR push semantics.
  • Stop flag round-trips: request → check → clear, all async-cache backed
  • PublishRunning injects synthetic Running step (not persisted), used for real-time UI progress
  • Remove clears cache and broadcasts removed event with distinct method name
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/IngestionRulesTests.csmedium
Business rule enforcement: state-gate validation (restart, stop, resume, start, delete) covering all status codes and edge cases.
  • IboGeneratedCode blocks restart (delivery already created) and stop (post-converge) — immutable once delivery exists
  • Resume only accepts Stopped + IsPausedInGracePeriod, not bare Stopped (guards non-grace pause paths)
  • Pipeline order (Reserve before Duplicate, Upload before Create) is encoded and verified
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/IngestionStepFailureTests.cslow
Error accumulation and serialization: category summarization, error payload JSON shape, and line-request-id semantics.
  • Error payload JSON is camelCase with summary field summarizing count and category breakdown
  • Delivery-level errors have null LineRequestId; line-level errors carry RequestId
  • Empty failure has no summary and IsFailed=false
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/MarkIngestionTerminallyFailedCommandHandlerTests.csmedium
Terminal failure hook: attempt supersession guard, terminal-status idempotence, and missing-ingestion grace.
  • Superseded attempt (newer attempt exists) is silently dropped
  • Already terminal ingestion (Succeeded) is idempotent no-op
  • Missing ingestion is ignored gracefully (no exception) — async fire-and-forget safety
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/ReserveKeysStepHandlerTests.cshigh
Key reservation guard: concurrent ingestion conflict detection via reservation service, failure flow without advancing.
  • Reserved key from another ingestion fails with correct error (key_concurrently_ingested implied)
  • No reservation conflict allows advance to DetectDuplicates; reservation is not released on success (kept until after upload)
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/ResumeInboundDeliveryIngestionCommandHandlerTests.cshigh
Grace-pause resume: re-arming grace with new token, same-attempt reuse, scheduled converge, and validation for grace-paused state.
  • Resume rejects non-grace Stopped (must not be called on early-stop paths) with DomainValidationException
  • New grace token is generated and embedded in scheduled CreateInboundDelivery command
  • Attempt remains same (reuse prepared keys); converge scheduled (not enqueued) on same delay as original
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/StartInboundDeliveryIngestionCommandHandlerTests.csmedium
Resume from Stopped (not grace): validation, same-attempt reuse, and grace-pause rejection to enforce Resume path.
  • Grace-paused ingestion (Stopped but IsPausedInGracePeriod=true) is rejected — must use Resume instead
  • Non-grace Stopped is allowed, keeps attempt number (reuse prepared keys), and clears stop flag
  • Saves once and broadcasts status change
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/StopInboundDeliveryIngestionCommandHandlerTests.cshigh
Stop request handling: in-grace synchronous Stopped persist (no flag), running-phase flag-raise, duplicate-request rejection, and validation guards.
  • Grace-paused stop persists Stopped directly without cache flag (synchronous, sole writer)
  • Running-phase stop uses cache flag (async pickup by next step); second stop while flag pending is rejected
  • Stop on non-active (already terminal/stopped) ingestion is rejected with DomainValidationException
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/SucceedIngestionWhenGeneratedOrderCompletedEventHandlerTests.csmedium
Event-driven finalization: ingestion waiting for generated order completion, idempotent no-op for stale/unrelated events.
  • Late/duplicate event when ingestion is already Succeeded is a no-op (no write)
  • Event with no matching IboGenerated ingestion is silently dropped (no write)
  • Completed order transitions ingestion from IboGenerated to Succeeded and broadcasts
src/WOCK.WholeSale.Tests/Deliveries/InboundDeliveryIngestion/UploadKeysStepHandlerTests.csmedium
Blob storage upload: file key upload with checksum verification, text-only skip, and mirror coordination.
  • Checksum mismatch throws exception (not business-failure) so Hangfire retries the upload
  • Text-only load skips step but still advances chain (pre-converge work complete)
  • Mirror async call is enqueued for each uploaded file without blocking step completion

Also check (outside the file list)

EF migrations

On the list at stage 17 — verify the migration Up() matches the configs and is forward-only; the *.Designer / snapshot files are EF-generated.

Wiring & DI

Stage 15 covers Startup/Program. Confirm the services/factories, the hosted config reloader, the Hangfire terminal-failure filter, and the SignalR/cron registrations are all present.

Shared aggregates

Stage 3 isolates the InboundDelivery/Key edits — review only those hunks (skip-key-upload gate, pre-computed blob naming), not the whole files.

The tester tool

Not part of the API PR: tools/ingestion-tester is a dev tool (this page lives in it).