kell_creations/kell_creations_apps/packages/feature_orders
Mike Kell 595d0b5a93 feat(stage-11b): custom order intake UI
Adds the operator-facing intake flow for made-to-order commissions on top of
the Stage 11A domain and data layers. The form, validation, and confirmation
receipt all live in feature_orders, so web and mobile host the same widget
rather than forking business logic per platform.

Stage 11C still covers the tracking page, the status-change timeline, and
inventory deduction on completion.

Application:

- CreateCustomOrder and RecordDeposit use cases wrapping the repository
  methods added in Stage 11A, so presentation never assembles a custom order
  aggregate by hand. RecordDeposit deliberately accepts no payment identifier.
- CustomOrderFormController with field-keyed validation, so each input renders
  its own error rather than a single form-level message. Clock and ID
  generator are injected, which keeps created-at stamps and order IDs
  deterministic under test.
- All money is typed as text and converted to integer cents by
  parseMoneyToCents before it reaches DepositInfo, consistent with
  PaymentResult.amountCents. Thousands separators, currency symbols, excess
  precision, and negatives are rejected or normalised at the boundary. The
  only double is the derived OrderItem.unitPrice, computed once from cents.
- Derived balance clamps at zero rather than reporting a credit when a deposit
  exceeds the quoted total, matching DepositInfo.balanceDueCents.

Presentation:

- CustomOrderFormPage — sectioned intake (customer, specification, pricing and
  deposit, reference images) over the shared design-system widgets.
- CustomOrderConfirmationPage — order summary, spec rendering, and a deposit
  receipt showing amount, method, date, and balance only.
- FulfillmentStatusChip over the design-system KcStatusChip, covering all
  seven pipeline values.
- showReferenceImageDialog() plus ReferenceImageDialogResult with URL
  validation and de-duplication.
- OrdersPage gains an optional onCreateCustomOrder callback. When null the
  "New Custom Order" action is not rendered, so the page stays usable in hosts
  that have no intake route.

Hosts:

- kell_web: new /orders/custom route rendering CustomOrderFormPage inside
  AppShell, with ordersToCustomOrder() and customOrderToOrders() handoffs on
  AppNavigation. Confirmation returns to Orders pre-selecting the new order.
- kell_mobile: new MobileCustomOrderPage, a full-screen SafeArea host for the
  shared form, reached from an extended FAB on the orders list. Market intake
  is the primary custom-order use case, so mobile gets a dedicated route
  rather than an inline panel. The list reloads on return.

Privacy:

- The deposit receipt renders amount, method, date, and balance only. The
  transaction ID stays on PaymentResult and is asserted absent from the
  rendered confirmation by test. The controller holds no payment identifier at
  any point.

Deferred to Stage 11C, recorded in the Stage 11B section:

- Product-base selection from the catalogue; the form captures a free-text
  specification instead.
- Platform camera and file-picker capture for reference images; the dialog
  accepts validated URLs only.

Validation:

- 146 new tests: 82 on CustomOrderFormController (money parsing, setters,
  derived balance and clamping, reference image add/remove/dedupe, the full
  validation matrix, buildSpec mapping, submission, quoted vs. collected
  deposit, repository failure, duplicate ID, reset), 26 on the form page, 24
  on the confirmation page including the no-payment-identifier assertion, 3 on
  the OrdersPage entry point, 6 on MobileCustomOrderPage, and 5 on the web
  route.
- 2,044/2,044 passing across all 11 packages and apps, up from the 1,898
  baseline at 0e3987b, with no regression.
- dart analyze --fatal-infos clean across all packages and apps.
- dart format --line-length 100 clean on every file touched by this slice.

Test-harness note: the intake form is taller than the default 800x600 test
view, so the submit-button tests resize tester.view rather than working around
a layout defect. The page scrolls normally on device.

Toolchain note: kell_web/pubspec.lock picked up SDK-pinned transitive bumps on
flutter pub get (matcher 0.12.19 to 0.12.20, meta 1.18.0 to 1.19.0, test_api
0.7.11 to 0.7.12, vector_math 2.2.0 to 2.4.3). Same accepted class as the
kell_mobile bumps recorded under chore/state-reconciliation-flutter-347;
committed so the lock file matches the toolchain in use.

Closes: P2.2 (intake UI portion)
2026-09-25 15:45:04 -04:00
..
build feat(orders): Stage 9A - WooCommerce Orders API integration 2026-07-11 09:45:36 -04:00
lib feat(stage-11b): custom order intake UI 2026-09-25 15:45:04 -04:00
test feat(stage-11b): custom order intake UI 2026-09-25 15:45:04 -04:00
.gitignore chore(feature_orders): add .gitignore, untrack .dart_tool and pubspec.lock 2026-05-30 10:09:40 -04:00
.metadata feat(stage-11a): custom order domain and data 2026-08-29 15:19:29 -04:00
CHANGELOG.md feat(stage-11a): custom order domain and data 2026-08-29 15:19:29 -04:00
LICENSE feat(stage-11a): custom order domain and data 2026-08-29 15:19:29 -04:00
README.md feat(stage-11b): custom order intake UI 2026-09-25 15:45:04 -04:00
analysis_options.yaml chore: reconcile working tree with main and re-baseline master documents 2026-09-25 14:19:36 -04:00
pubspec.yaml feat(stage-11a): custom order domain and data 2026-08-29 15:19:29 -04:00

README.md

Order management for the Kell Creations operations platform. Covers both standard catalogue orders and made-to-order custom commissions behind a single Order aggregate and one OrdersRepository contract.

Features

  • Domain model — Order, OrderItem, OrderStatus, OrderSource, OrderType, PaymentMethod, PaymentResult.
  • Custom orders — CustomOrderSpec (material, colour, size, dimensions, reference images, notes, estimated completion) and DepositInfo (deposit and total amounts in integer cents, deposit/settlement dates, derived balance).
  • Fulfillment pipeline — FulfillmentStatus with enforced transitions: quoted → depositReceived → inProduction → qualityCheck → readyForPickup → completed, plus cancelled from any non-terminal stage and a qualityCheck → inProduction rework path. Illegal jumps are rejected by the domain, not silently applied.
  • Repository contract — OrdersRepository with read, status, note, and create operations plus createCustomOrder, updateCustomOrderSpec, recordDeposit, and advanceFulfillmentStatus.
  • Implementations — FakeOrdersRepository (seeded standard and custom orders across every pipeline stage) and WooCommerceOrdersRepository (REST API v3, custom order state round-tripped through _kc_-prefixed order meta-data).
  • Application layer — OrdersController, CheckoutController, CustomOrderFormController, and the GetOrders / UpdateOrderStatus / AddOrderNote / CreateOrder / CreateCustomOrder / RecordDeposit use cases.
  • Presentation — OrdersPage, OrderCard, OrderDetailPanel, OrderStatusChip, CustomOrderFormPage, CustomOrderConfirmationPage, FulfillmentStatusChip, showReferenceImageDialog.

Getting started

Add the package as a path dependency and import the barrel:

dependencies:
  feature_orders:
    path: ../feature_orders
import 'package:feature_orders/feature_orders.dart';

Usage

final repository = FakeOrdersRepository(logger: observability.logger);

final result = await repository.createCustomOrder(
  order,
  spec: CustomOrderSpec(
    material: 'Organic cotton',
    color: 'Sage green',
    size: 'Large',
  ),
  deposit: const DepositInfo.quoted(
    totalAmountCents: 7200,
    depositAmountCents: 2400,
  ),
);

// Advance through the pipeline — illegal jumps are rejected.
await repository.recordDeposit(
  order.id,
  amountCents: 2400,
  paidAt: DateTime.now(),
);
await repository.advanceFulfillmentStatus(order.id, FulfillmentStatus.inProduction);

Custom order intake UI

CustomOrderFormPage captures an entire commission in one screen — customer contact, description, specification, reference images, pricing and deposit, and the estimated completion date. It owns a CustomOrderFormController unless one is injected, so a route or a shell can mount it with only a repository:

CustomOrderFormPage(
  repository: services.ordersRepository,
  onComplete: (order) => Navigator.of(context).pop(order),
);

Money is typed as free text and converted to integer cents by CustomOrderFormController.parseMoneyToCents before it reaches the domain, so no double arithmetic ever decides a price. On success the page swaps to CustomOrderConfirmationPage, which renders the deposit receipt.

Observability and privacy

Repository implementations accept an optional KcLogger from core. Failed and rejected write operations are logged with the operation name, order ID, and status names only. Customer identity, addresses, money amounts, and deposit payment identifiers are never logged, and every context map is additionally passed through KcRedactor. There are no silent catch (_) {} blocks.

The WooCommerce meta-data mapping deliberately excludes the deposit transaction ID — it lives on PaymentResult alone. See the class-level documentation on WooCommerceOrderMapper for the complete key table.

Additional information

Part of the kell_creations_apps monorepo. Run the package tests with flutter test from this directory, or the whole suite with ./tools/run_all_tests.sh --analyze from kell_creations_apps/.