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

183 lines
5.9 KiB
Dart

import 'package:uuid/uuid.dart';
import '../models/order_models.dart';
import 'company_profile_service.dart';
import 'sales_order_repository.dart';
class SalesOrderLineInput {
const SalesOrderLineInput({
required this.description,
required this.quantity,
required this.unitPrice,
this.productId,
this.taxRate,
});
final String description;
final int quantity;
final int unitPrice;
final String? productId;
final double? taxRate;
}
class SalesOrderService {
SalesOrderService({
SalesOrderRepository? repository,
CompanyProfileService? companyProfileService,
}) : _repository = repository ?? SalesOrderRepository(),
_companyProfileService = companyProfileService ?? CompanyProfileService();
final SalesOrderRepository _repository;
final CompanyProfileService _companyProfileService;
final Uuid _uuid = const Uuid();
static const Map<SalesOrderStatus, List<SalesOrderStatus>> _transitions = {
SalesOrderStatus.draft: [SalesOrderStatus.confirmed, SalesOrderStatus.cancelled],
SalesOrderStatus.confirmed: [SalesOrderStatus.picking, SalesOrderStatus.cancelled],
SalesOrderStatus.picking: [SalesOrderStatus.shipped, SalesOrderStatus.cancelled],
SalesOrderStatus.shipped: [SalesOrderStatus.closed],
SalesOrderStatus.closed: [],
SalesOrderStatus.cancelled: [],
};
Future<List<SalesOrder>> fetchOrders({SalesOrderStatus? status}) {
return _repository.fetchOrders(status: status);
}
Future<SalesOrder> createOrder({
required String customerId,
required String customerName,
List<SalesOrderLineInput> lines = const [],
DateTime? requestedShipDate,
String? notes,
String? assignedTo,
}) async {
final profile = await _companyProfileService.loadProfile();
final now = DateTime.now();
final orderId = _uuid.v4();
final lineItems = _buildItems(orderId, lines);
final order = SalesOrder(
id: orderId,
orderNumber: _generateOrderNumber(now),
customerId: customerId,
customerNameSnapshot: customerName,
orderDate: now,
requestedShipDate: requestedShipDate,
status: SalesOrderStatus.draft,
subtotal: 0,
taxAmount: 0,
totalAmount: 0,
notes: notes,
assignedTo: assignedTo,
workflowStage: _workflowStage(SalesOrderStatus.draft),
createdAt: now,
updatedAt: now,
items: lineItems,
).recalculateTotals(defaultTaxRate: profile.taxRate);
await _repository.upsertOrder(order);
return order;
}
Future<SalesOrder> updateOrder(
SalesOrder order, {
List<SalesOrderLineInput>? replacedLines,
DateTime? requestedShipDate,
String? notes,
String? assignedTo,
}) async {
final profile = await _companyProfileService.loadProfile();
final now = DateTime.now();
final nextItems = replacedLines != null ? _buildItems(order.id, replacedLines) : order.items;
final updated = order
.copyWith(
requestedShipDate: requestedShipDate ?? order.requestedShipDate,
notes: notes ?? order.notes,
assignedTo: assignedTo ?? order.assignedTo,
updatedAt: now,
items: nextItems,
)
.recalculateTotals(defaultTaxRate: profile.taxRate);
await _repository.upsertOrder(updated);
return updated;
}
Future<SalesOrder> transitionStatus(String orderId, SalesOrderStatus nextStatus, {bool force = false}) async {
final order = await _repository.findById(orderId);
if (order == null) {
throw StateError('order not found: $orderId');
}
if (!force && !_canTransition(order.status, nextStatus)) {
throw StateError('invalid transition ${order.status.name} -> ${nextStatus.name}');
}
final now = DateTime.now();
final updated = order.copyWith(
status: nextStatus,
workflowStage: _workflowStage(nextStatus),
updatedAt: now,
);
await _repository.upsertOrder(updated);
return updated;
}
Future<SalesOrder> advanceStatus(String orderId) async {
final order = await _repository.findById(orderId);
if (order == null) {
throw StateError('order not found: $orderId');
}
final candidates = _transitions[order.status];
if (candidates == null || candidates.isEmpty) {
return order;
}
return transitionStatus(orderId, candidates.first);
}
bool _canTransition(SalesOrderStatus current, SalesOrderStatus next) {
final allowed = _transitions[current];
return allowed?.contains(next) ?? false;
}
List<SalesOrderStatus> nextStatuses(SalesOrderStatus current) {
return List.unmodifiable(_transitions[current] ?? const []);
}
List<SalesOrderItem> _buildItems(String orderId, List<SalesOrderLineInput> lines) {
return lines.asMap().entries.map((entry) {
final index = entry.key;
final line = entry.value;
return SalesOrderItem(
id: _uuid.v4(),
orderId: orderId,
productId: line.productId,
description: line.description,
quantity: line.quantity,
unitPrice: line.unitPrice,
taxRate: line.taxRate ?? 0,
sortIndex: index,
);
}).toList();
}
String _generateOrderNumber(DateTime timestamp) {
final datePart = '${timestamp.year}${timestamp.month.toString().padLeft(2, '0')}${timestamp.day.toString().padLeft(2, '0')}';
final timePart = '${timestamp.hour.toString().padLeft(2, '0')}${timestamp.minute.toString().padLeft(2, '0')}';
return 'SO$datePart-$timePart-${timestamp.millisecondsSinceEpoch % 1000}'.toUpperCase();
}
String _workflowStage(SalesOrderStatus status) {
switch (status) {
case SalesOrderStatus.draft:
return 'order';
case SalesOrderStatus.confirmed:
return 'ready';
case SalesOrderStatus.picking:
return 'picking';
case SalesOrderStatus.shipped:
return 'shipping';
case SalesOrderStatus.closed:
return 'closed';
case SalesOrderStatus.cancelled:
return 'cancelled';
}
}
}