183 lines
6.3 KiB
Dart
183 lines
6.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../models/shipment_models.dart';
|
|
import 'business_calendar_mapper.dart';
|
|
import 'sales_order_repository.dart';
|
|
import 'shipment_repository.dart';
|
|
|
|
class ShipmentLineInput {
|
|
const ShipmentLineInput({
|
|
required this.description,
|
|
required this.quantity,
|
|
this.orderItemId,
|
|
});
|
|
|
|
final String description;
|
|
final int quantity;
|
|
final String? orderItemId;
|
|
}
|
|
|
|
class ShipmentService {
|
|
ShipmentService({
|
|
ShipmentRepository? shipmentRepository,
|
|
SalesOrderRepository? orderRepository,
|
|
BusinessCalendarMapper? calendarMapper,
|
|
}) : _shipmentRepository = shipmentRepository ?? ShipmentRepository(),
|
|
_orderRepository = orderRepository ?? SalesOrderRepository(),
|
|
_calendarMapper = calendarMapper ?? BusinessCalendarMapper();
|
|
|
|
final ShipmentRepository _shipmentRepository;
|
|
final SalesOrderRepository _orderRepository;
|
|
final BusinessCalendarMapper _calendarMapper;
|
|
final Uuid _uuid = const Uuid();
|
|
|
|
static const Map<ShipmentStatus, List<ShipmentStatus>> _transitions = {
|
|
ShipmentStatus.pending: [ShipmentStatus.picking, ShipmentStatus.cancelled],
|
|
ShipmentStatus.picking: [ShipmentStatus.ready, ShipmentStatus.cancelled],
|
|
ShipmentStatus.ready: [ShipmentStatus.shipped, ShipmentStatus.cancelled],
|
|
ShipmentStatus.shipped: [ShipmentStatus.delivered],
|
|
ShipmentStatus.delivered: [],
|
|
ShipmentStatus.cancelled: [],
|
|
};
|
|
|
|
Future<List<Shipment>> fetchShipments({ShipmentStatus? status}) {
|
|
return _shipmentRepository.fetchShipments(status: status);
|
|
}
|
|
|
|
Future<Shipment> createShipment({
|
|
String? orderId,
|
|
String? orderNumberSnapshot,
|
|
String? customerNameSnapshot,
|
|
List<ShipmentLineInput> lines = const [],
|
|
DateTime? scheduledShipDate,
|
|
DateTime? actualShipDate,
|
|
String? carrierName,
|
|
String? trackingNumber,
|
|
String? trackingUrl,
|
|
String? labelPdfPath,
|
|
String? notes,
|
|
}) async {
|
|
String? resolvedOrderId = orderId;
|
|
String? resolvedOrderNumber = orderNumberSnapshot;
|
|
String? resolvedCustomerName = customerNameSnapshot;
|
|
|
|
if (resolvedOrderId != null && (resolvedOrderNumber == null || resolvedCustomerName == null)) {
|
|
final order = await _orderRepository.findById(resolvedOrderId);
|
|
if (order != null) {
|
|
resolvedOrderNumber = order.orderNumber ?? order.id.substring(0, 6);
|
|
resolvedCustomerName = order.customerNameSnapshot;
|
|
}
|
|
}
|
|
|
|
resolvedCustomerName ??= '未設定';
|
|
|
|
final now = DateTime.now();
|
|
final shipmentId = _uuid.v4();
|
|
final shipment = Shipment(
|
|
id: shipmentId,
|
|
orderId: resolvedOrderId,
|
|
orderNumberSnapshot: resolvedOrderNumber,
|
|
customerNameSnapshot: resolvedCustomerName,
|
|
scheduledShipDate: scheduledShipDate,
|
|
actualShipDate: actualShipDate,
|
|
status: ShipmentStatus.pending,
|
|
carrierName: carrierName,
|
|
trackingNumber: trackingNumber,
|
|
trackingUrl: trackingUrl,
|
|
labelPdfPath: labelPdfPath,
|
|
notes: notes,
|
|
pickingCompletedAt: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
items: _buildItems(shipmentId, lines),
|
|
);
|
|
await _shipmentRepository.upsertShipment(shipment);
|
|
unawaited(_calendarMapper.syncShipment(shipment));
|
|
return shipment;
|
|
}
|
|
|
|
Future<Shipment> updateShipment(
|
|
Shipment shipment, {
|
|
List<ShipmentLineInput>? replacedLines,
|
|
DateTime? scheduledShipDate,
|
|
DateTime? actualShipDate,
|
|
String? carrierName,
|
|
String? trackingNumber,
|
|
String? trackingUrl,
|
|
String? labelPdfPath,
|
|
String? notes,
|
|
}) async {
|
|
final updated = shipment.copyWith(
|
|
scheduledShipDate: scheduledShipDate ?? shipment.scheduledShipDate,
|
|
actualShipDate: actualShipDate ?? shipment.actualShipDate,
|
|
carrierName: carrierName ?? shipment.carrierName,
|
|
trackingNumber: trackingNumber ?? shipment.trackingNumber,
|
|
trackingUrl: trackingUrl ?? shipment.trackingUrl,
|
|
labelPdfPath: labelPdfPath ?? shipment.labelPdfPath,
|
|
notes: notes ?? shipment.notes,
|
|
updatedAt: DateTime.now(),
|
|
items: replacedLines != null ? _buildItems(shipment.id, replacedLines) : shipment.items,
|
|
);
|
|
await _shipmentRepository.upsertShipment(updated);
|
|
unawaited(_calendarMapper.syncShipment(updated));
|
|
return updated;
|
|
}
|
|
|
|
Future<Shipment> transitionStatus(String shipmentId, ShipmentStatus nextStatus, {bool force = false}) async {
|
|
final shipment = await _shipmentRepository.findById(shipmentId);
|
|
if (shipment == null) {
|
|
throw StateError('shipment not found: $shipmentId');
|
|
}
|
|
if (!force && !_canTransition(shipment.status, nextStatus)) {
|
|
throw StateError('invalid transition ${shipment.status.name} -> ${nextStatus.name}');
|
|
}
|
|
final now = DateTime.now();
|
|
final updated = shipment.copyWith(
|
|
status: nextStatus,
|
|
updatedAt: now,
|
|
actualShipDate: nextStatus == ShipmentStatus.shipped || nextStatus == ShipmentStatus.delivered
|
|
? (shipment.actualShipDate ?? now)
|
|
: shipment.actualShipDate,
|
|
pickingCompletedAt: nextStatus == ShipmentStatus.ready ? (shipment.pickingCompletedAt ?? now) : shipment.pickingCompletedAt,
|
|
);
|
|
await _shipmentRepository.upsertShipment(updated);
|
|
unawaited(_calendarMapper.syncShipment(updated));
|
|
return updated;
|
|
}
|
|
|
|
Future<Shipment> advanceStatus(String shipmentId) async {
|
|
final shipment = await _shipmentRepository.findById(shipmentId);
|
|
if (shipment == null) {
|
|
throw StateError('shipment not found: $shipmentId');
|
|
}
|
|
final candidates = _transitions[shipment.status];
|
|
if (candidates == null || candidates.isEmpty) {
|
|
return shipment;
|
|
}
|
|
return transitionStatus(shipmentId, candidates.first);
|
|
}
|
|
|
|
List<ShipmentStatus> nextStatuses(ShipmentStatus current) {
|
|
return List.unmodifiable(_transitions[current] ?? const []);
|
|
}
|
|
|
|
bool _canTransition(ShipmentStatus current, ShipmentStatus next) {
|
|
final allowed = _transitions[current];
|
|
return allowed?.contains(next) ?? false;
|
|
}
|
|
|
|
List<ShipmentItem> _buildItems(String shipmentId, List<ShipmentLineInput> lines) {
|
|
return lines.asMap().entries.map((entry) {
|
|
final line = entry.value;
|
|
return ShipmentItem(
|
|
id: _uuid.v4(),
|
|
shipmentId: shipmentId,
|
|
orderItemId: line.orderItemId,
|
|
description: line.description,
|
|
quantity: line.quantity,
|
|
);
|
|
}).toList();
|
|
}
|
|
}
|