Compare commits
No commits in common. "b2df206123cd0033f6883cdc5f892336e2a2c552" and "b61c886c23ca1e7b6885a200a786e722241a98c6" have entirely different histories.
b2df206123
...
b61c886c23
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
## Current status
|
||||
|
||||
- main baseline updated through: feat/inventory-domain-expansion (Stage 5A complete)
|
||||
- next branch: feat/inventory-page-ui (Stage 5B) or feat/orders-domain (Stage 5C)
|
||||
- current stage: Stage 5A complete — inventory domain expansion with write actions
|
||||
- main baseline updated through: bulk-status-actions (Stage 7A complete)
|
||||
- next branch: feat/bulk-operator-workflows (Stage 7B) or feat/integrations-contracts (Stage 8A)
|
||||
- current stage: Stage 7A complete — deciding next slice
|
||||
|
||||
## Slice tracker
|
||||
|
||||
|
|
@ -242,33 +242,9 @@
|
|||
- analyze: passed (info-level only — 8 pre-existing unnecessary_import in test files)
|
||||
- brief updated: yes
|
||||
|
||||
### feat/inventory-domain-expansion
|
||||
|
||||
- status: merged to main
|
||||
- date: 2026-07-10
|
||||
- inspection: complete
|
||||
- implementation: complete
|
||||
- files changed:
|
||||
- `feature_inventory/lib/src/domain/inventory_item.dart` — expanded model with `description`, `imageUrl`, `category`, `reorderPoint`, `supplier`, `location`, `lastUpdated` fields; added `copyWith()`
|
||||
- `feature_inventory/lib/src/domain/inventory_adjustment_result.dart` — new value object with `success`, `updatedItem`, `errorMessage`; named constructors `success()` and `failure()`
|
||||
- `feature_inventory/lib/src/domain/inventory_repository.dart` — expanded contract with `adjustQuantity()`, `setQuantity()`, `createItem()`, `updateItem()`, `updateItemImage()`
|
||||
- `feature_inventory/lib/src/data/fake_inventory_repository.dart` — expanded seed data (6 items with full fields, supplier/location, reorderPoint); implemented all write methods with status auto-update logic
|
||||
- `feature_inventory/lib/src/application/adjust_inventory_quantity.dart` — new use case wrapping `adjustQuantity()`
|
||||
- `feature_inventory/lib/src/application/create_inventory_item.dart` — new use case wrapping `createItem()`
|
||||
- `feature_inventory/lib/src/application/update_inventory_item.dart` — new use case wrapping `updateItem()`
|
||||
- `feature_inventory/lib/src/application/inventory_controller.dart` — added `isUpdating`, `lastActionResult`, `adjustQuantity()`, `createItem()`, `updateItem()`, `_refreshItem()` with pre-filter selectedItem refresh
|
||||
- `feature_inventory/lib/src/presentation/inventory_page.dart` — updated `InventoryController` constructor call to pass all 4 use cases
|
||||
- `feature_inventory/lib/feature_inventory.dart` — expanded barrel exports for all new types and use cases
|
||||
- `feature_inventory/test/feature_inventory_test.dart` — 49 tests covering InventoryStatus, InventoryItem, InventoryAdjustmentResult, FakeInventoryRepository (all write methods), and InventoryController (read + write actions)
|
||||
- `kell_web/test/dashboard/domain/dashboard_summary_test.dart` — updated InventoryItem construction to use expanded model
|
||||
- `kell_web/test/dashboard/application/dashboard_controller_test.dart` — updated stub InventoryRepository to implement expanded contract
|
||||
- tests: passed (49/49 feature_inventory, 24/24 kell_web, 20/20 core, 41/41 design_system, 311/311 feature_wordpress — 445 total)
|
||||
- analyze: passed (dart analyze — no issues found)
|
||||
- brief updated: yes
|
||||
|
||||
### chore/analyze-cleanup-and-tracker-sync
|
||||
|
||||
- status: merged to main
|
||||
- status: in progress
|
||||
- date: 2026-05-30
|
||||
- inspection: complete
|
||||
- implementation: complete
|
||||
|
|
|
|||
|
|
@ -185,10 +185,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -246,10 +246,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -270,10 +270,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -185,10 +185,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -246,10 +246,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -270,10 +270,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -14,24 +14,6 @@ class _StubInventoryRepository implements InventoryRepository {
|
|||
|
||||
@override
|
||||
Future<List<InventoryItem>> getInventoryItems() async => items;
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> adjustQuantity(String id, int delta, String reason) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> setQuantity(String id, int quantity, String reason) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryItem> createItem(InventoryItem item) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryItem> updateItem(InventoryItem item) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> updateItemImage(String id, String imageUrl) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
class _StubProductPublishingRepository implements ProductPublishingRepository {
|
||||
|
|
@ -74,52 +56,6 @@ class _StubOrdersRepository implements OrdersRepository {
|
|||
class _FailingInventoryRepository implements InventoryRepository {
|
||||
@override
|
||||
Future<List<InventoryItem>> getInventoryItems() => throw Exception('network error');
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> adjustQuantity(String id, int delta, String reason) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> setQuantity(String id, int quantity, String reason) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryItem> createItem(InventoryItem item) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryItem> updateItem(InventoryItem item) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> updateItemImage(String id, String imageUrl) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
// ── Shared test helpers ────────────────────────────────────────────────────
|
||||
|
||||
final _baseDate = DateTime(2026, 7, 1);
|
||||
|
||||
InventoryItem _inv({
|
||||
required String id,
|
||||
required String sku,
|
||||
required String name,
|
||||
required int quantityOnHand,
|
||||
required double unitPrice,
|
||||
required InventoryStatus status,
|
||||
int reorderPoint = 5,
|
||||
}) {
|
||||
return InventoryItem(
|
||||
id: id,
|
||||
sku: sku,
|
||||
name: name,
|
||||
description: '',
|
||||
imageUrl: '',
|
||||
category: 'Test',
|
||||
quantityOnHand: quantityOnHand,
|
||||
unitPrice: unitPrice,
|
||||
reorderPoint: reorderPoint,
|
||||
status: status,
|
||||
lastUpdated: _baseDate,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -142,7 +78,7 @@ void main() {
|
|||
|
||||
test('load() sets isLoading then populates summary', () async {
|
||||
final inventoryItems = [
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '1',
|
||||
sku: 'A',
|
||||
name: 'A',
|
||||
|
|
@ -150,7 +86,7 @@ void main() {
|
|||
unitPrice: 5.0,
|
||||
status: InventoryStatus.inStock,
|
||||
),
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '2',
|
||||
sku: 'B',
|
||||
name: 'B',
|
||||
|
|
@ -202,7 +138,7 @@ void main() {
|
|||
|
||||
test('load() aggregates data from all three repositories', () async {
|
||||
final inventoryItems = [
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '1',
|
||||
sku: 'A',
|
||||
name: 'A',
|
||||
|
|
|
|||
|
|
@ -4,34 +4,6 @@ import 'package:feature_wordpress/feature_wordpress.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_web/dashboard/domain/dashboard_summary.dart';
|
||||
|
||||
// ── Shared test helpers ────────────────────────────────────────────────────
|
||||
|
||||
final _baseDate = DateTime(2026, 7, 1);
|
||||
|
||||
InventoryItem _inv({
|
||||
required String id,
|
||||
required String sku,
|
||||
required String name,
|
||||
required int quantityOnHand,
|
||||
required double unitPrice,
|
||||
required InventoryStatus status,
|
||||
int reorderPoint = 5,
|
||||
}) {
|
||||
return InventoryItem(
|
||||
id: id,
|
||||
sku: sku,
|
||||
name: name,
|
||||
description: '',
|
||||
imageUrl: '',
|
||||
category: 'Test',
|
||||
quantityOnHand: quantityOnHand,
|
||||
unitPrice: unitPrice,
|
||||
reorderPoint: reorderPoint,
|
||||
status: status,
|
||||
lastUpdated: _baseDate,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('DashboardSummary.empty', () {
|
||||
test('has all zeroes', () {
|
||||
|
|
@ -51,7 +23,7 @@ void main() {
|
|||
group('DashboardSummary.fromData', () {
|
||||
test('counts inventory statuses correctly', () {
|
||||
final items = [
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '1',
|
||||
sku: 'A',
|
||||
name: 'A',
|
||||
|
|
@ -59,7 +31,7 @@ void main() {
|
|||
unitPrice: 5.0,
|
||||
status: InventoryStatus.inStock,
|
||||
),
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '2',
|
||||
sku: 'B',
|
||||
name: 'B',
|
||||
|
|
@ -67,7 +39,7 @@ void main() {
|
|||
unitPrice: 5.0,
|
||||
status: InventoryStatus.lowStock,
|
||||
),
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '3',
|
||||
sku: 'C',
|
||||
name: 'C',
|
||||
|
|
@ -75,7 +47,7 @@ void main() {
|
|||
unitPrice: 5.0,
|
||||
status: InventoryStatus.outOfStock,
|
||||
),
|
||||
_inv(
|
||||
const InventoryItem(
|
||||
id: '4',
|
||||
sku: 'D',
|
||||
name: 'D',
|
||||
|
|
@ -205,9 +177,10 @@ void main() {
|
|||
});
|
||||
|
||||
test('computes full summary from fake repository data', () {
|
||||
// Use the same data the fake repositories return.
|
||||
final summary = DashboardSummary.fromData(
|
||||
inventoryItems: [
|
||||
_inv(
|
||||
inventoryItems: const [
|
||||
InventoryItem(
|
||||
id: '1',
|
||||
sku: 'BC-FLR-001',
|
||||
name: 'Floral Bowl Cozy',
|
||||
|
|
@ -215,16 +188,15 @@ void main() {
|
|||
unitPrice: 12.99,
|
||||
status: InventoryStatus.inStock,
|
||||
),
|
||||
_inv(
|
||||
InventoryItem(
|
||||
id: '2',
|
||||
sku: 'CS-CIT-002',
|
||||
name: 'Citrus Coaster Set',
|
||||
quantityOnHand: 7,
|
||||
unitPrice: 16.50,
|
||||
status: InventoryStatus.lowStock,
|
||||
reorderPoint: 10,
|
||||
),
|
||||
_inv(
|
||||
InventoryItem(
|
||||
id: '3',
|
||||
sku: 'NL-OCN-003',
|
||||
name: 'Ocean Nightlight',
|
||||
|
|
@ -232,7 +204,7 @@ void main() {
|
|||
unitPrice: 19.99,
|
||||
status: InventoryStatus.outOfStock,
|
||||
),
|
||||
_inv(
|
||||
InventoryItem(
|
||||
id: '4',
|
||||
sku: 'JG-BLU-004',
|
||||
name: 'Fabric Jar Gripper',
|
||||
|
|
@ -240,7 +212,7 @@ void main() {
|
|||
unitPrice: 8.50,
|
||||
status: InventoryStatus.inStock,
|
||||
),
|
||||
_inv(
|
||||
InventoryItem(
|
||||
id: '5',
|
||||
sku: 'SH-SUN-005',
|
||||
name: 'Skillet Handle Sleeve',
|
||||
|
|
@ -248,7 +220,7 @@ void main() {
|
|||
unitPrice: 10.99,
|
||||
status: InventoryStatus.lowStock,
|
||||
),
|
||||
_inv(
|
||||
InventoryItem(
|
||||
id: '6',
|
||||
sku: 'SC-SUB-006',
|
||||
name: 'Sublimated Slate Coaster',
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
library;
|
||||
|
||||
export 'src/application/adjust_inventory_quantity.dart';
|
||||
export 'src/application/create_inventory_item.dart';
|
||||
export 'src/application/get_inventory_items.dart';
|
||||
export 'src/application/inventory_controller.dart';
|
||||
export 'src/application/update_inventory_item.dart';
|
||||
export 'src/data/fake_inventory_repository.dart';
|
||||
export 'src/domain/inventory_adjustment_result.dart';
|
||||
export 'src/domain/inventory_item.dart';
|
||||
export 'src/domain/inventory_repository.dart';
|
||||
export 'src/domain/inventory_status.dart';
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import '../domain/inventory_adjustment_result.dart';
|
||||
import '../domain/inventory_repository.dart';
|
||||
|
||||
/// Use case: adjust the quantity of an inventory item by a delta.
|
||||
///
|
||||
/// A positive [delta] adds stock; a negative [delta] removes stock.
|
||||
class AdjustInventoryQuantity {
|
||||
final InventoryRepository repository;
|
||||
|
||||
AdjustInventoryQuantity(this.repository);
|
||||
|
||||
Future<InventoryAdjustmentResult> call(String id, int delta, String reason) =>
|
||||
repository.adjustQuantity(id, delta, reason);
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import '../domain/inventory_item.dart';
|
||||
import '../domain/inventory_repository.dart';
|
||||
|
||||
/// Use case: create a new inventory item.
|
||||
class CreateInventoryItem {
|
||||
final InventoryRepository repository;
|
||||
|
||||
CreateInventoryItem(this.repository);
|
||||
|
||||
Future<InventoryItem> call(InventoryItem item) => repository.createItem(item);
|
||||
}
|
||||
|
|
@ -1,34 +1,18 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import '../domain/inventory_adjustment_result.dart';
|
||||
import '../domain/inventory_item.dart';
|
||||
import '../domain/inventory_status.dart';
|
||||
import 'adjust_inventory_quantity.dart';
|
||||
import 'create_inventory_item.dart';
|
||||
import 'get_inventory_items.dart';
|
||||
import 'update_inventory_item.dart';
|
||||
|
||||
/// Controller that manages the inventory workspace state, including
|
||||
/// filtering by status, free-text search, item selection, and write actions.
|
||||
/// filtering by status, free-text search, and item selection.
|
||||
class InventoryController extends ChangeNotifier {
|
||||
final GetInventoryItems _getInventoryItems;
|
||||
final AdjustInventoryQuantity _adjustInventoryQuantity;
|
||||
final CreateInventoryItem _createInventoryItem;
|
||||
final UpdateInventoryItem _updateInventoryItem;
|
||||
|
||||
InventoryController(
|
||||
this._getInventoryItems,
|
||||
this._adjustInventoryQuantity,
|
||||
this._createInventoryItem,
|
||||
this._updateInventoryItem,
|
||||
);
|
||||
InventoryController(this._getInventoryItems);
|
||||
|
||||
bool isLoading = false;
|
||||
bool isUpdating = false;
|
||||
Object? error;
|
||||
|
||||
/// The last write action result, or `null` if no write has been performed.
|
||||
InventoryAdjustmentResult? lastActionResult;
|
||||
|
||||
/// All items returned by the repository (unfiltered).
|
||||
List<InventoryItem> _allItems = [];
|
||||
|
||||
|
|
@ -94,83 +78,7 @@ class InventoryController extends ChangeNotifier {
|
|||
return false;
|
||||
}
|
||||
|
||||
// ── Write actions ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Adjusts the quantity of the item with [id] by [delta].
|
||||
///
|
||||
/// A positive [delta] adds stock; a negative [delta] removes stock.
|
||||
/// Sets [lastActionResult] and refreshes the list on success.
|
||||
Future<void> adjustQuantity(String id, int delta, String reason) async {
|
||||
isUpdating = true;
|
||||
lastActionResult = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _adjustInventoryQuantity(id, delta, reason);
|
||||
lastActionResult = result;
|
||||
if (result.success) {
|
||||
await _refreshItem(result.updatedItem!);
|
||||
}
|
||||
} catch (e) {
|
||||
lastActionResult = InventoryAdjustmentResult.failure(e.toString());
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new inventory item and adds it to the list.
|
||||
Future<void> createItem(InventoryItem item) async {
|
||||
isUpdating = true;
|
||||
lastActionResult = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final created = await _createInventoryItem(item);
|
||||
_allItems = [..._allItems, created];
|
||||
_applyFilters();
|
||||
lastActionResult = InventoryAdjustmentResult.success(created);
|
||||
} catch (e) {
|
||||
lastActionResult = InventoryAdjustmentResult.failure(e.toString());
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing inventory item and refreshes the list.
|
||||
Future<void> updateItem(InventoryItem item) async {
|
||||
isUpdating = true;
|
||||
lastActionResult = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final updated = await _updateInventoryItem(item);
|
||||
await _refreshItem(updated);
|
||||
lastActionResult = InventoryAdjustmentResult.success(updated);
|
||||
} catch (e) {
|
||||
lastActionResult = InventoryAdjustmentResult.failure(e.toString());
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Replaces the item in [_allItems] and re-applies filters.
|
||||
/// Also refreshes [selectedItem] if it matches the updated item.
|
||||
Future<void> _refreshItem(InventoryItem updated) async {
|
||||
// Update selectedItem BEFORE _applyFilters so the contains() check passes.
|
||||
if (selectedItem?.id == updated.id) {
|
||||
selectedItem = updated;
|
||||
}
|
||||
_allItems = [
|
||||
for (final i in _allItems)
|
||||
if (i.id == updated.id) updated else i,
|
||||
];
|
||||
_applyFilters();
|
||||
}
|
||||
// ── Private helpers ────────────────────────────────────────────────────
|
||||
|
||||
void _applyFilters() {
|
||||
var result = _allItems;
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import '../domain/inventory_item.dart';
|
||||
import '../domain/inventory_repository.dart';
|
||||
|
||||
/// Use case: update an existing inventory item.
|
||||
class UpdateInventoryItem {
|
||||
final InventoryRepository repository;
|
||||
|
||||
UpdateInventoryItem(this.repository);
|
||||
|
||||
Future<InventoryItem> call(InventoryItem item) => repository.updateItem(item);
|
||||
}
|
||||
|
|
@ -1,191 +1,61 @@
|
|||
import '../domain/inventory_adjustment_result.dart';
|
||||
import '../domain/inventory_item.dart';
|
||||
import '../domain/inventory_repository.dart';
|
||||
import '../domain/inventory_status.dart';
|
||||
|
||||
class FakeInventoryRepository implements InventoryRepository {
|
||||
/// Mutable in-memory store — seeded on construction.
|
||||
final List<InventoryItem> _items = _buildSeedData();
|
||||
@override
|
||||
Future<List<InventoryItem>> getInventoryItems() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
static List<InventoryItem> _buildSeedData() {
|
||||
final now = DateTime(2026, 7, 1);
|
||||
return [
|
||||
return const [
|
||||
InventoryItem(
|
||||
id: '1',
|
||||
sku: 'BC-FLR-001',
|
||||
name: 'Floral Bowl Cozy',
|
||||
description: 'Hand-sewn fabric bowl cozy with floral print. Microwave safe.',
|
||||
imageUrl: 'https://example.com/images/bc-flr-001.jpg',
|
||||
category: 'Kitchen Accessories',
|
||||
quantityOnHand: 18,
|
||||
unitPrice: 12.99,
|
||||
reorderPoint: 5,
|
||||
supplier: 'Kell Creations',
|
||||
location: 'Shelf A1',
|
||||
status: InventoryStatus.inStock,
|
||||
lastUpdated: now,
|
||||
),
|
||||
InventoryItem(
|
||||
id: '2',
|
||||
sku: 'CS-CIT-002',
|
||||
name: 'Citrus Coaster Set',
|
||||
description: 'Set of 4 sublimated slate coasters with citrus fruit designs.',
|
||||
imageUrl: 'https://example.com/images/cs-cit-002.jpg',
|
||||
category: 'Home Decor',
|
||||
quantityOnHand: 7,
|
||||
unitPrice: 16.50,
|
||||
reorderPoint: 10,
|
||||
supplier: 'Kell Creations',
|
||||
location: 'Shelf B2',
|
||||
status: InventoryStatus.lowStock,
|
||||
lastUpdated: now,
|
||||
),
|
||||
InventoryItem(
|
||||
id: '3',
|
||||
sku: 'NL-OCN-003',
|
||||
name: 'Ocean Nightlight',
|
||||
description: 'Sublimated acrylic nightlight with ocean wave design.',
|
||||
imageUrl: 'https://example.com/images/nl-ocn-003.jpg',
|
||||
category: 'Home Decor',
|
||||
quantityOnHand: 0,
|
||||
unitPrice: 19.99,
|
||||
reorderPoint: 3,
|
||||
supplier: null,
|
||||
location: 'Shelf C1',
|
||||
status: InventoryStatus.outOfStock,
|
||||
lastUpdated: now,
|
||||
),
|
||||
InventoryItem(
|
||||
id: '4',
|
||||
sku: 'JG-BLU-004',
|
||||
name: 'Fabric Jar Gripper',
|
||||
description: 'Quilted fabric jar gripper for easy lid opening.',
|
||||
imageUrl: 'https://example.com/images/jg-blu-004.jpg',
|
||||
category: 'Kitchen Accessories',
|
||||
quantityOnHand: 23,
|
||||
unitPrice: 8.50,
|
||||
reorderPoint: 8,
|
||||
supplier: 'Kell Creations',
|
||||
location: 'Shelf A2',
|
||||
status: InventoryStatus.inStock,
|
||||
lastUpdated: now,
|
||||
),
|
||||
InventoryItem(
|
||||
id: '5',
|
||||
sku: 'SH-SUN-005',
|
||||
name: 'Skillet Handle Sleeve',
|
||||
description: 'Heat-resistant fabric sleeve for cast iron skillet handles.',
|
||||
imageUrl: 'https://example.com/images/sh-sun-005.jpg',
|
||||
category: 'Kitchen Accessories',
|
||||
quantityOnHand: 5,
|
||||
unitPrice: 10.99,
|
||||
reorderPoint: 8,
|
||||
supplier: 'Kell Creations',
|
||||
location: null,
|
||||
status: InventoryStatus.lowStock,
|
||||
lastUpdated: now,
|
||||
),
|
||||
InventoryItem(
|
||||
id: '6',
|
||||
sku: 'SC-SUB-006',
|
||||
name: 'Sublimated Slate Coaster',
|
||||
description: 'Single sublimated slate coaster — prototype/draft item.',
|
||||
imageUrl: '',
|
||||
category: 'Home Decor',
|
||||
quantityOnHand: 0,
|
||||
unitPrice: 14.99,
|
||||
reorderPoint: 5,
|
||||
supplier: null,
|
||||
location: null,
|
||||
status: InventoryStatus.draft,
|
||||
lastUpdated: now,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<InventoryItem>> getInventoryItems() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
return List.unmodifiable(_items);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> adjustQuantity(String id, int delta, String reason) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
final index = _items.indexWhere((i) => i.id == id);
|
||||
if (index == -1) {
|
||||
return const InventoryAdjustmentResult.failure('Item not found');
|
||||
}
|
||||
final item = _items[index];
|
||||
final newQty = item.quantityOnHand + delta;
|
||||
if (newQty < 0) {
|
||||
return const InventoryAdjustmentResult.failure('Quantity cannot go below zero');
|
||||
}
|
||||
final updated = item.copyWith(
|
||||
quantityOnHand: newQty,
|
||||
status: _deriveStatus(newQty, item.reorderPoint),
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
_items[index] = updated;
|
||||
return InventoryAdjustmentResult.success(updated);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> setQuantity(String id, int quantity, String reason) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
if (quantity < 0) {
|
||||
return const InventoryAdjustmentResult.failure('Quantity cannot be negative');
|
||||
}
|
||||
final index = _items.indexWhere((i) => i.id == id);
|
||||
if (index == -1) {
|
||||
return const InventoryAdjustmentResult.failure('Item not found');
|
||||
}
|
||||
final item = _items[index];
|
||||
final updated = item.copyWith(
|
||||
quantityOnHand: quantity,
|
||||
status: _deriveStatus(quantity, item.reorderPoint),
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
_items[index] = updated;
|
||||
return InventoryAdjustmentResult.success(updated);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InventoryItem> createItem(InventoryItem item) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
_items.add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InventoryItem> updateItem(InventoryItem item) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
final index = _items.indexWhere((i) => i.id == item.id);
|
||||
if (index == -1) {
|
||||
throw StateError('Item with id ${item.id} not found');
|
||||
}
|
||||
final updated = item.copyWith(lastUpdated: DateTime.now());
|
||||
_items[index] = updated;
|
||||
return updated;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InventoryAdjustmentResult> updateItemImage(String id, String imageUrl) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
final index = _items.indexWhere((i) => i.id == id);
|
||||
if (index == -1) {
|
||||
return const InventoryAdjustmentResult.failure('Item not found');
|
||||
}
|
||||
final updated = _items[index].copyWith(imageUrl: imageUrl, lastUpdated: DateTime.now());
|
||||
_items[index] = updated;
|
||||
return InventoryAdjustmentResult.success(updated);
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────────
|
||||
|
||||
static InventoryStatus _deriveStatus(int quantity, int reorderPoint) {
|
||||
if (quantity == 0) return InventoryStatus.outOfStock;
|
||||
if (quantity <= reorderPoint) return InventoryStatus.lowStock;
|
||||
return InventoryStatus.inStock;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
import 'inventory_item.dart';
|
||||
|
||||
/// The result of an inventory quantity adjustment or write operation.
|
||||
class InventoryAdjustmentResult {
|
||||
/// Whether the operation succeeded.
|
||||
final bool success;
|
||||
|
||||
/// The updated [InventoryItem] after the operation, or `null` on failure.
|
||||
final InventoryItem? updatedItem;
|
||||
|
||||
/// A human-readable error message when [success] is `false`.
|
||||
final String? errorMessage;
|
||||
|
||||
const InventoryAdjustmentResult.success(InventoryItem item)
|
||||
: success = true,
|
||||
updatedItem = item,
|
||||
errorMessage = null;
|
||||
|
||||
const InventoryAdjustmentResult.failure(String message)
|
||||
: success = false,
|
||||
updatedItem = null,
|
||||
errorMessage = message;
|
||||
}
|
||||
|
|
@ -1,67 +1,19 @@
|
|||
import 'inventory_status.dart';
|
||||
|
||||
/// A single inventory item tracked by the Kell Creations operations platform.
|
||||
class InventoryItem {
|
||||
final String id;
|
||||
final String sku;
|
||||
final String name;
|
||||
final String description;
|
||||
final String imageUrl;
|
||||
final String category;
|
||||
final int quantityOnHand;
|
||||
final double unitPrice;
|
||||
final int reorderPoint;
|
||||
final String? supplier;
|
||||
final String? location;
|
||||
final InventoryStatus status;
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const InventoryItem({
|
||||
required this.id,
|
||||
required this.sku,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.imageUrl,
|
||||
required this.category,
|
||||
required this.quantityOnHand,
|
||||
required this.unitPrice,
|
||||
required this.reorderPoint,
|
||||
this.supplier,
|
||||
this.location,
|
||||
required this.status,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
/// Returns a copy of this [InventoryItem] with the given fields replaced.
|
||||
InventoryItem copyWith({
|
||||
String? id,
|
||||
String? sku,
|
||||
String? name,
|
||||
String? description,
|
||||
String? imageUrl,
|
||||
String? category,
|
||||
int? quantityOnHand,
|
||||
double? unitPrice,
|
||||
int? reorderPoint,
|
||||
String? supplier,
|
||||
String? location,
|
||||
InventoryStatus? status,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return InventoryItem(
|
||||
id: id ?? this.id,
|
||||
sku: sku ?? this.sku,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
category: category ?? this.category,
|
||||
quantityOnHand: quantityOnHand ?? this.quantityOnHand,
|
||||
unitPrice: unitPrice ?? this.unitPrice,
|
||||
reorderPoint: reorderPoint ?? this.reorderPoint,
|
||||
supplier: supplier ?? this.supplier,
|
||||
location: location ?? this.location,
|
||||
status: status ?? this.status,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,5 @@
|
|||
import 'inventory_adjustment_result.dart';
|
||||
import 'inventory_item.dart';
|
||||
|
||||
abstract class InventoryRepository {
|
||||
/// Returns all inventory items.
|
||||
Future<List<InventoryItem>> getInventoryItems();
|
||||
|
||||
/// Adjusts the quantity of an item by [delta] (positive = add, negative = remove).
|
||||
/// Returns an [InventoryAdjustmentResult] indicating success or failure.
|
||||
Future<InventoryAdjustmentResult> adjustQuantity(String id, int delta, String reason);
|
||||
|
||||
/// Sets the quantity of an item to an absolute [quantity].
|
||||
/// Returns an [InventoryAdjustmentResult] indicating success or failure.
|
||||
Future<InventoryAdjustmentResult> setQuantity(String id, int quantity, String reason);
|
||||
|
||||
/// Creates a new inventory item.
|
||||
/// Returns the created [InventoryItem].
|
||||
Future<InventoryItem> createItem(InventoryItem item);
|
||||
|
||||
/// Updates an existing inventory item with all fields from [item].
|
||||
/// Returns the updated [InventoryItem].
|
||||
Future<InventoryItem> updateItem(InventoryItem item);
|
||||
|
||||
/// Updates the image URL for an inventory item.
|
||||
/// Returns an [InventoryAdjustmentResult] indicating success or failure.
|
||||
Future<InventoryAdjustmentResult> updateItemImage(String id, String imageUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/adjust_inventory_quantity.dart';
|
||||
import '../application/create_inventory_item.dart';
|
||||
import '../application/get_inventory_items.dart';
|
||||
import '../application/inventory_controller.dart';
|
||||
import '../application/update_inventory_item.dart';
|
||||
import '../domain/inventory_repository.dart';
|
||||
import 'widgets/inventory_item_card.dart';
|
||||
|
||||
|
|
@ -44,12 +41,7 @@ class _InventoryPageState extends State<InventoryPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = InventoryController(
|
||||
GetInventoryItems(widget.repository),
|
||||
AdjustInventoryQuantity(widget.repository),
|
||||
CreateInventoryItem(widget.repository),
|
||||
UpdateInventoryItem(widget.repository),
|
||||
);
|
||||
controller = InventoryController(GetInventoryItems(widget.repository));
|
||||
|
||||
// Apply any initial filter / query before loading.
|
||||
if (widget.initialFilter != null) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name: feature_inventory
|
||||
description: "Inventory management feature for the Kell Creations operations platform. Provides domain model, repository contract, fake data layer, and write operations for inventory items."
|
||||
description: "A new Flutter package project."
|
||||
version: 0.0.1
|
||||
publish_to: "none"
|
||||
homepage:
|
||||
|
|
|
|||
|
|
@ -1,52 +1,10 @@
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
InventoryController _makeController(FakeInventoryRepository repo) {
|
||||
return InventoryController(
|
||||
GetInventoryItems(repo),
|
||||
AdjustInventoryQuantity(repo),
|
||||
CreateInventoryItem(repo),
|
||||
UpdateInventoryItem(repo),
|
||||
);
|
||||
}
|
||||
|
||||
InventoryItem _makeItem({
|
||||
String id = 'test-1',
|
||||
String sku = 'TEST-001',
|
||||
String name = 'Test Item',
|
||||
String description = 'A test item',
|
||||
String imageUrl = '',
|
||||
String category = 'Test Category',
|
||||
int quantityOnHand = 10,
|
||||
double unitPrice = 9.99,
|
||||
int reorderPoint = 3,
|
||||
String? supplier,
|
||||
String? location,
|
||||
InventoryStatus status = InventoryStatus.inStock,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return InventoryItem(
|
||||
id: id,
|
||||
sku: sku,
|
||||
name: name,
|
||||
description: description,
|
||||
imageUrl: imageUrl,
|
||||
category: category,
|
||||
quantityOnHand: quantityOnHand,
|
||||
unitPrice: unitPrice,
|
||||
reorderPoint: reorderPoint,
|
||||
supplier: supplier,
|
||||
location: location,
|
||||
status: status,
|
||||
lastUpdated: lastUpdated ?? DateTime(2026, 7, 1),
|
||||
);
|
||||
}
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_inventory/src/application/get_inventory_items.dart';
|
||||
import 'package:feature_inventory/src/application/inventory_controller.dart';
|
||||
|
||||
void main() {
|
||||
// ── InventoryStatus ────────────────────────────────────────────────────────
|
||||
|
||||
group('InventoryStatus', () {
|
||||
test('has four values', () {
|
||||
expect(InventoryStatus.values.length, 4);
|
||||
|
|
@ -60,88 +18,6 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
// ── InventoryItem ──────────────────────────────────────────────────────────
|
||||
|
||||
group('InventoryItem', () {
|
||||
test('constructs with all required fields', () {
|
||||
final item = _makeItem();
|
||||
expect(item.id, 'test-1');
|
||||
expect(item.sku, 'TEST-001');
|
||||
expect(item.name, 'Test Item');
|
||||
expect(item.description, 'A test item');
|
||||
expect(item.imageUrl, '');
|
||||
expect(item.category, 'Test Category');
|
||||
expect(item.quantityOnHand, 10);
|
||||
expect(item.unitPrice, 9.99);
|
||||
expect(item.reorderPoint, 3);
|
||||
expect(item.supplier, isNull);
|
||||
expect(item.location, isNull);
|
||||
expect(item.status, InventoryStatus.inStock);
|
||||
});
|
||||
|
||||
test('copyWith returns new instance with updated fields', () {
|
||||
final original = _makeItem();
|
||||
final copy = original.copyWith(name: 'Updated', quantityOnHand: 20);
|
||||
expect(copy.name, 'Updated');
|
||||
expect(copy.quantityOnHand, 20);
|
||||
// Unchanged fields preserved
|
||||
expect(copy.id, original.id);
|
||||
expect(copy.sku, original.sku);
|
||||
expect(copy.description, original.description);
|
||||
expect(copy.category, original.category);
|
||||
expect(copy.unitPrice, original.unitPrice);
|
||||
expect(copy.reorderPoint, original.reorderPoint);
|
||||
expect(copy.status, original.status);
|
||||
});
|
||||
|
||||
test('copyWith preserves nullable fields when not specified', () {
|
||||
final item = _makeItem(supplier: 'Kell Creations', location: 'Shelf A1');
|
||||
final copy = item.copyWith(name: 'New Name');
|
||||
expect(copy.supplier, 'Kell Creations');
|
||||
expect(copy.location, 'Shelf A1');
|
||||
});
|
||||
|
||||
test('copyWith can update nullable fields', () {
|
||||
final item = _makeItem();
|
||||
final copy = item.copyWith(supplier: 'New Supplier', location: 'Bin 3');
|
||||
expect(copy.supplier, 'New Supplier');
|
||||
expect(copy.location, 'Bin 3');
|
||||
});
|
||||
|
||||
test('copyWith can update status', () {
|
||||
final item = _makeItem(status: InventoryStatus.inStock);
|
||||
final copy = item.copyWith(status: InventoryStatus.lowStock);
|
||||
expect(copy.status, InventoryStatus.lowStock);
|
||||
});
|
||||
|
||||
test('copyWith can update imageUrl', () {
|
||||
final item = _makeItem(imageUrl: '');
|
||||
final copy = item.copyWith(imageUrl: 'https://example.com/img.jpg');
|
||||
expect(copy.imageUrl, 'https://example.com/img.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
// ── InventoryAdjustmentResult ──────────────────────────────────────────────
|
||||
|
||||
group('InventoryAdjustmentResult', () {
|
||||
test('success result has correct fields', () {
|
||||
final item = _makeItem();
|
||||
final result = InventoryAdjustmentResult.success(item);
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem, item);
|
||||
expect(result.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('failure result has correct fields', () {
|
||||
const result = InventoryAdjustmentResult.failure('Something went wrong');
|
||||
expect(result.success, false);
|
||||
expect(result.updatedItem, isNull);
|
||||
expect(result.errorMessage, 'Something went wrong');
|
||||
});
|
||||
});
|
||||
|
||||
// ── FakeInventoryRepository ────────────────────────────────────────────────
|
||||
|
||||
group('FakeInventoryRepository', () {
|
||||
late FakeInventoryRepository repository;
|
||||
|
||||
|
|
@ -161,134 +37,15 @@ void main() {
|
|||
expect(names, contains('Citrus Coaster Set'));
|
||||
expect(names, contains('Ocean Nightlight'));
|
||||
});
|
||||
|
||||
test('seed data includes expanded fields', () async {
|
||||
final items = await repository.getInventoryItems();
|
||||
final first = items.first;
|
||||
expect(first.description, isNotEmpty);
|
||||
expect(first.category, isNotEmpty);
|
||||
expect(first.reorderPoint, greaterThan(0));
|
||||
expect(first.lastUpdated, isNotNull);
|
||||
});
|
||||
|
||||
test('seed data has items with supplier and location', () async {
|
||||
final items = await repository.getInventoryItems();
|
||||
final withSupplier = items.where((i) => i.supplier != null).toList();
|
||||
expect(withSupplier, isNotEmpty);
|
||||
});
|
||||
|
||||
test('adjustQuantity adds stock successfully', () async {
|
||||
final result = await repository.adjustQuantity('1', 5, 'restock');
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.quantityOnHand, 23); // 18 + 5
|
||||
});
|
||||
|
||||
test('adjustQuantity removes stock successfully', () async {
|
||||
final result = await repository.adjustQuantity('1', -3, 'sold');
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.quantityOnHand, 15); // 18 - 3
|
||||
});
|
||||
|
||||
test('adjustQuantity fails when result would be negative', () async {
|
||||
final result = await repository.adjustQuantity('1', -100, 'error');
|
||||
expect(result.success, false);
|
||||
expect(result.errorMessage, isNotEmpty);
|
||||
});
|
||||
|
||||
test('adjustQuantity fails for unknown id', () async {
|
||||
final result = await repository.adjustQuantity('UNKNOWN', 1, 'test');
|
||||
expect(result.success, false);
|
||||
});
|
||||
|
||||
test('adjustQuantity updates status to outOfStock when qty reaches 0', () async {
|
||||
// Item 3 has qty 0 already; use item 4 (qty 23, reorderPoint 8)
|
||||
// Reduce to 0
|
||||
final result = await repository.adjustQuantity('4', -23, 'clear');
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.status, InventoryStatus.outOfStock);
|
||||
});
|
||||
|
||||
test('adjustQuantity updates status to lowStock when qty at or below reorderPoint', () async {
|
||||
// Item 1: qty 18, reorderPoint 5 — reduce to 4 (below reorderPoint)
|
||||
final result = await repository.adjustQuantity('1', -14, 'reduce');
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.status, InventoryStatus.lowStock);
|
||||
});
|
||||
|
||||
test('adjustQuantity updates status to inStock when qty above reorderPoint', () async {
|
||||
// Item 2: qty 7, reorderPoint 10 — add enough to go above reorderPoint
|
||||
final result = await repository.adjustQuantity('2', 10, 'restock');
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.status, InventoryStatus.inStock);
|
||||
});
|
||||
|
||||
test('setQuantity sets absolute quantity', () async {
|
||||
final result = await repository.setQuantity('1', 50, 'inventory count');
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.quantityOnHand, 50);
|
||||
});
|
||||
|
||||
test('setQuantity fails for negative quantity', () async {
|
||||
final result = await repository.setQuantity('1', -1, 'error');
|
||||
expect(result.success, false);
|
||||
});
|
||||
|
||||
test('setQuantity fails for unknown id', () async {
|
||||
final result = await repository.setQuantity('UNKNOWN', 10, 'test');
|
||||
expect(result.success, false);
|
||||
});
|
||||
|
||||
test('createItem adds item to repository', () async {
|
||||
final newItem = _makeItem(id: 'new-1', sku: 'NEW-001', name: 'New Item');
|
||||
await repository.createItem(newItem);
|
||||
final items = await repository.getInventoryItems();
|
||||
expect(items.length, 7);
|
||||
expect(items.any((i) => i.id == 'new-1'), true);
|
||||
});
|
||||
|
||||
test('updateItem updates existing item', () async {
|
||||
final items = await repository.getInventoryItems();
|
||||
final original = items.first;
|
||||
final updated = original.copyWith(name: 'Updated Name', unitPrice: 99.99);
|
||||
final result = await repository.updateItem(updated);
|
||||
expect(result.name, 'Updated Name');
|
||||
expect(result.unitPrice, 99.99);
|
||||
});
|
||||
|
||||
test('updateItem throws for unknown id', () async {
|
||||
final unknown = _makeItem(id: 'UNKNOWN');
|
||||
expect(() => repository.updateItem(unknown), throwsStateError);
|
||||
});
|
||||
|
||||
test('updateItemImage updates image URL', () async {
|
||||
const newUrl = 'https://example.com/new-image.jpg';
|
||||
final result = await repository.updateItemImage('1', newUrl);
|
||||
expect(result.success, true);
|
||||
expect(result.updatedItem!.imageUrl, newUrl);
|
||||
});
|
||||
|
||||
test('updateItemImage fails for unknown id', () async {
|
||||
final result = await repository.updateItemImage('UNKNOWN', 'url');
|
||||
expect(result.success, false);
|
||||
});
|
||||
|
||||
test('adjustQuantity persists change across calls', () async {
|
||||
await repository.adjustQuantity('1', -5, 'sold');
|
||||
final items = await repository.getInventoryItems();
|
||||
final item = items.firstWhere((i) => i.id == '1');
|
||||
expect(item.quantityOnHand, 13); // 18 - 5
|
||||
});
|
||||
});
|
||||
|
||||
// ── InventoryController ────────────────────────────────────────────────────
|
||||
|
||||
group('InventoryController', () {
|
||||
late FakeInventoryRepository repository;
|
||||
late InventoryController controller;
|
||||
|
||||
setUp(() {
|
||||
repository = FakeInventoryRepository();
|
||||
controller = _makeController(repository);
|
||||
controller = InventoryController(GetInventoryItems(repository));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
|
|
@ -297,13 +54,11 @@ void main() {
|
|||
|
||||
test('starts with empty state', () {
|
||||
expect(controller.isLoading, false);
|
||||
expect(controller.isUpdating, false);
|
||||
expect(controller.items, isEmpty);
|
||||
expect(controller.selectedItem, isNull);
|
||||
expect(controller.activeFilter, isNull);
|
||||
expect(controller.searchQuery, '');
|
||||
expect(controller.error, isNull);
|
||||
expect(controller.lastActionResult, isNull);
|
||||
});
|
||||
|
||||
test('load populates items', () async {
|
||||
|
|
@ -400,85 +155,5 @@ void main() {
|
|||
controller.setFilter('lowStock');
|
||||
expect(controller.selectedItem, isNull);
|
||||
});
|
||||
|
||||
// ── Write action tests ─────────────────────────────────────────────────
|
||||
|
||||
test('adjustQuantity succeeds and updates item in list', () async {
|
||||
await controller.load();
|
||||
|
||||
await controller.adjustQuantity('1', 5, 'restock');
|
||||
|
||||
expect(controller.isUpdating, false);
|
||||
expect(controller.lastActionResult, isNotNull);
|
||||
expect(controller.lastActionResult!.success, true);
|
||||
final item = controller.items.firstWhere((i) => i.id == '1');
|
||||
expect(item.quantityOnHand, 23); // 18 + 5
|
||||
});
|
||||
|
||||
test('adjustQuantity failure sets lastActionResult with error', () async {
|
||||
await controller.load();
|
||||
|
||||
await controller.adjustQuantity('1', -1000, 'error');
|
||||
|
||||
expect(controller.lastActionResult!.success, false);
|
||||
expect(controller.lastActionResult!.errorMessage, isNotEmpty);
|
||||
});
|
||||
|
||||
test('adjustQuantity refreshes selectedItem when it is the adjusted item', () async {
|
||||
await controller.load();
|
||||
controller.selectBySku('BC-FLR-001');
|
||||
expect(controller.selectedItem!.quantityOnHand, 18);
|
||||
|
||||
await controller.adjustQuantity('1', 2, 'restock');
|
||||
|
||||
expect(controller.selectedItem!.quantityOnHand, 20);
|
||||
});
|
||||
|
||||
test('createItem adds item to list', () async {
|
||||
await controller.load();
|
||||
expect(controller.items.length, 6);
|
||||
|
||||
final newItem = _makeItem(id: 'new-1', sku: 'NEW-001', name: 'New Item');
|
||||
await controller.createItem(newItem);
|
||||
|
||||
expect(controller.items.length, 7);
|
||||
expect(controller.lastActionResult!.success, true);
|
||||
});
|
||||
|
||||
test('updateItem updates item in list', () async {
|
||||
await controller.load();
|
||||
final original = controller.items.firstWhere((i) => i.id == '1');
|
||||
final updated = original.copyWith(name: 'Updated Name');
|
||||
|
||||
await controller.updateItem(updated);
|
||||
|
||||
expect(controller.lastActionResult!.success, true);
|
||||
final inList = controller.items.firstWhere((i) => i.id == '1');
|
||||
expect(inList.name, 'Updated Name');
|
||||
});
|
||||
|
||||
test('updateItem refreshes selectedItem when it is the updated item', () async {
|
||||
await controller.load();
|
||||
controller.selectBySku('BC-FLR-001');
|
||||
final original = controller.selectedItem!;
|
||||
final updated = original.copyWith(name: 'Refreshed Name');
|
||||
|
||||
await controller.updateItem(updated);
|
||||
|
||||
expect(controller.selectedItem!.name, 'Refreshed Name');
|
||||
});
|
||||
|
||||
test('isUpdating is true during write and false after', () async {
|
||||
await controller.load();
|
||||
|
||||
final states = <bool>[];
|
||||
controller.addListener(() => states.add(controller.isUpdating));
|
||||
|
||||
await controller.adjustQuantity('1', 1, 'test');
|
||||
|
||||
// First notification: isUpdating = true, last: isUpdating = false
|
||||
expect(states.first, true);
|
||||
expect(states.last, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue