Skip to content

Feature/application slices - #10

Merged
finn-abel merged 10 commits into
mainfrom
feature/application-slices
Aug 11, 2026
Merged

Feature/application slices#10
finn-abel merged 10 commits into
mainfrom
feature/application-slices

Conversation

@finn-abel

Copy link
Copy Markdown
Contributor

Phase 7 — Complete application-layer feature slices (Steps 32–40)

Summary

Phase 7 completes the application layer with 16 commands and queries covering the full call-to-cash workflow:

  • Customer and service-location management
  • Technician creation, skills, shifts, and listing
  • Job creation, status changes, and manual assignment
  • Day optimization and emergency-job insertion
  • Dispatch-board projection
  • Invoice generation and payment recording

The scheduling engine now operates against persisted data, and an integration test exercises the complete workflow from customer creation through payment.

No controllers or public API contracts are included; those remain scheduled for Steps 47–49.

Main changes

Application

Added feature slices for:

  • Customers/CreateCustomer
  • Customers/AddServiceLocation
  • Customers/GetCustomer
  • Customers/ListCustomers
  • Technicians/CreateTechnician
  • Technicians/SetSkills
  • Technicians/SetShift
  • Technicians/ListTechnicians
  • Jobs/CreateJob
  • Jobs/ChangeJobStatus
  • Jobs/AssignJob
  • Scheduling/OptimizeDay
  • Scheduling/InsertJob
  • Dispatch/GetBoard
  • Invoicing/GenerateInvoice
  • Invoicing/MarkPaid

Each slice includes its command/query, handler, validator where applicable, result projections, and typed errors.

AddApplication discovers all 16 handlers and 14 validators through assembly scanning.

Domain

Added:

  • Technician.SetSkills
  • Job.CanTransition
  • Job.MarkInvoiced
  • Job.MarkPaid
  • Shared StopPlacement logic enforcing one stop per job

The job transition theory now covers all 72 status combinations.

Infrastructure

Added:

  • SystemClock
  • Unified AddInfrastructure registration
  • IScheduler → AnnealingScheduler
  • DispatchBoardReadModel
  • FakePaymentGateway

The dispatch board is produced with three fixed queries rather than loading aggregate graphs or issuing per-row queries.

Testing support

Added reusable application-test fakes and a SliceHost that execute requests through the real MediatR pipeline, validators, scheduling engine, and travel provider.

A reflection-based request-shape test prevents commands and queries from accepting OrgId or raw Guid values. Tenant identity must come from ITenantContext.

Important implementation decisions

Payment ordering

MarkPaid charges the payment gateway before changing invoice and job state. This avoids recording payment when the gateway refuses the charge.

There is still a known risk when a real processor is introduced: payment could succeed before the database transaction fails. ChargeAsync accepts the invoice identity to support idempotent retries, but durable payment-reference storage and reconciliation remain future work.

The current fake gateway is intentional v1 product scope because real payment processing is explicitly excluded.

Scheduling precision

Planned timestamps are truncated to PostgreSQL’s microsecond precision inside StopPlacement.

Without this, a schedule read from PostgreSQL differs slightly from the same schedule recomputed in .NET, causing unchanged stops to appear moved. Integration coverage confirms that optimizing an unchanged day publishes no AssignmentChanged events.

Dropped scheduled jobs

When optimization can no longer place a job, its stop is removed but its status remains Scheduled because the state machine has no Scheduled → Unscheduled transition.

The job remains visible in the dispatch board’s unassigned pile, but this is the main unresolved edge in the phase. A complete fix requires Job.Unschedule(), a contract-table change, regeneration, and a client release.

Manual skill overrides

Manual assignment does not enforce technician skill matching. The optimizer still treats skill matching as a hard constraint, but a dispatcher may override it when manually placing work.

Reviewer feedback is specifically requested on this choice. A stricter alternative would return a conflict unless the command contains an explicit override flag.

Manual assignment does not re-time the route

Moving a stop manually recalculates that stop’s sequence and travel time but does not re-time all later stops. Full route timing remains the scheduling engine’s responsibility.

The dispatch board therefore orders stops by scheduled start and treats sequence as a label rather than assuming it is always contiguous.

Commands carry primitives for validated values

Commands use primitives for coordinates, time windows, and objective weights. Validators reject malformed values before handlers construct domain value objects.

This prevents expected input errors from escaping as domain exceptions and becoming HTTP 500 responses. Strongly typed IDs remain on commands because an unknown ID is a handler-level NotFound result rather than malformed input.

UTC conversion

External DateTimeOffset values are converted to UTC before persistence because Npgsql rejects non-zero offsets for timestamptz.

This currently occurs in four handlers. A future persistence convention could centralize the conversion, although scheduling precision must still be normalized before domain comparison.

Domain events for invoicing

Job.MarkInvoiced and Job.MarkPaid do not publish separate job events. Invoice creation and InvoicePaid already describe those actions, so publishing equivalent events from both aggregates would duplicate one business occurrence.

Testing

All checks pass:

  • 553 total tests
  • 496 unit tests
  • 57 integration tests
  • Zero analyzer or compiler warnings in Debug and Release
  • Nullable reference types and warnings-as-errors enabled
  • make check-contracts clean

Coverage includes:

  • Customer, technician, job, scheduling, dispatch, and invoicing slices
  • Full call-to-cash integration flow
  • Tenant isolation
  • PostgreSQL/PostGIS round trips
  • Non-UTC timestamp inputs
  • Transactional updates across multiple aggregates
  • One-stop-per-job enforcement
  • Deterministic scheduling
  • Emergency insertion affecting only the selected lane
  • No-op optimization publishing no events
  • Dispatch-board construction in exactly three queries
  • Payment refusal and successful payment flows
  • Validation and domain-transition consistency

Run locally with:

make test
make test-fast
make check-contracts

Reviewer focus

Feedback is most useful on:

  1. Allowing manual assignment to override technician skill requirements.
  2. Deferring the Scheduled → Unscheduled transition for optimizer-dropped jobs.
  3. Converting timestamps to UTC in handlers instead of through an EF convention.
  4. Charging before commit and deferring payment-reference persistence.
  5. Allowing sub-cent unit prices to round to whole cents before multiplying by quantity.

Deliberately excluded

This phase does not include:

  • Controllers, API DTOs, or public endpoints
  • SignalR dispatch updates
  • Mobile synchronization
  • Automatic invoicing after job completion
  • Real payment processing
  • Additional update, delete, or paging operations not named by Steps 32–40

@finn-abel
finn-abel merged commit c5d80d5 into main Aug 11, 2026
2 checks passed
@finn-abel
finn-abel deleted the feature/application-slices branch August 11, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant