h-1.flutter.0/lib/services/shipment_service.dart
2026-03-04 14:55:40 +09:00

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();
}
}