refactor: 인벤토리 테이블 스펙과 도메인 계층 정비
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/stock_transaction.dart';
|
||||
import '../../domain/entities/stock_transaction_input.dart';
|
||||
import '../../domain/repositories/stock_transaction_repository.dart';
|
||||
import '../dtos/stock_transaction_dto.dart';
|
||||
|
||||
/// 재고 트랜잭션 API를 호출하는 원격 저장소 구현체.
|
||||
class StockTransactionRepositoryRemote implements StockTransactionRepository {
|
||||
StockTransactionRepositoryRemote({required ApiClient apiClient})
|
||||
: _api = apiClient;
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '${ApiRoutes.apiV1}/stock-transactions';
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<StockTransaction>> list({
|
||||
StockTransactionListFilter? filter,
|
||||
}) async {
|
||||
final effectiveFilter = filter ?? StockTransactionListFilter();
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
query: effectiveFilter.toQuery(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return StockTransactionDto.parsePaginated(response.data ?? const {});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> fetchDetail(
|
||||
int id, {
|
||||
List<String> include = const ['lines', 'customers', 'approval'],
|
||||
}) async {
|
||||
final response = await _api.get<Map<String, dynamic>>(
|
||||
'$_basePath/$id',
|
||||
query: {if (include.isNotEmpty) 'include': include.join(',')},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> create(StockTransactionCreateInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> update(
|
||||
int id,
|
||||
StockTransactionUpdateInput input,
|
||||
) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
'$_basePath/$id',
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _api.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> restore(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/restore',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> submit(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/submit',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> complete(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/complete',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> approve(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/approve',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> reject(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/reject',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> cancel(int id) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$id/cancel',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseSingle(response.data);
|
||||
}
|
||||
|
||||
StockTransaction _parseSingle(Map<String, dynamic>? body) {
|
||||
final data = _extractData(body);
|
||||
return StockTransactionDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractData(Map<String, dynamic>? body) {
|
||||
if (body == null) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
if (body['data'] is Map<String, dynamic>) {
|
||||
return body['data'] as Map<String, dynamic>;
|
||||
}
|
||||
if (body.containsKey('transaction')) {
|
||||
final transaction = body['transaction'];
|
||||
if (transaction is Map<String, dynamic>) {
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/stock_transaction.dart';
|
||||
import '../../domain/entities/stock_transaction_input.dart';
|
||||
import '../../domain/repositories/stock_transaction_repository.dart';
|
||||
import '../dtos/stock_transaction_dto.dart';
|
||||
|
||||
/// 재고 트랜잭션 고객 연결 API를 호출하는 원격 저장소 구현체.
|
||||
class TransactionCustomerRepositoryRemote
|
||||
implements TransactionCustomerRepository {
|
||||
TransactionCustomerRepositoryRemote({required ApiClient apiClient})
|
||||
: _api = apiClient;
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '${ApiRoutes.apiV1}/stock-transactions';
|
||||
static const _customerPath = '${ApiRoutes.apiV1}/transaction-customers';
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionCustomer>> addCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerCreateInput> customers,
|
||||
) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$transactionId/customers',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
'customers': customers
|
||||
.map((customer) => customer.toJson())
|
||||
.toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseCustomers(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionCustomer>> updateCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerUpdateInput> customers,
|
||||
) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
'$_basePath/$transactionId/customers',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
'customers': customers
|
||||
.map((customer) => customer.toJson())
|
||||
.toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseCustomers(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteCustomer(int customerLinkId) async {
|
||||
await _api.delete<void>('$_customerPath/$customerLinkId');
|
||||
}
|
||||
|
||||
List<StockTransactionCustomer> _parseCustomers(Map<String, dynamic>? body) {
|
||||
final data = _extractData(body);
|
||||
if (data['customers'] is List) {
|
||||
final dto = StockTransactionDto.fromJson(data);
|
||||
return dto.customers;
|
||||
}
|
||||
if (data.containsKey('id')) {
|
||||
final dto = StockTransactionDto.fromJson({
|
||||
'customers': [data],
|
||||
});
|
||||
return dto.customers;
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractData(Map<String, dynamic>? body) {
|
||||
if (body == null) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
if (body['data'] is Map<String, dynamic>) {
|
||||
return body['data'] as Map<String, dynamic>;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/core/network/api_routes.dart';
|
||||
|
||||
import '../../domain/entities/stock_transaction.dart';
|
||||
import '../../domain/entities/stock_transaction_input.dart';
|
||||
import '../../domain/repositories/stock_transaction_repository.dart';
|
||||
import '../dtos/stock_transaction_dto.dart';
|
||||
|
||||
/// 재고 트랜잭션 라인 API를 호출하는 원격 저장소 구현체.
|
||||
class TransactionLineRepositoryRemote implements TransactionLineRepository {
|
||||
TransactionLineRepositoryRemote({required ApiClient apiClient})
|
||||
: _api = apiClient;
|
||||
|
||||
final ApiClient _api;
|
||||
|
||||
static const _basePath = '${ApiRoutes.apiV1}/stock-transactions';
|
||||
static const _linePath = '${ApiRoutes.apiV1}/transaction-lines';
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionLine>> addLines(
|
||||
int transactionId,
|
||||
List<TransactionLineCreateInput> lines,
|
||||
) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_basePath/$transactionId/lines',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
'lines': lines.map((line) => line.toJson()).toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseLines(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StockTransactionLine>> updateLines(
|
||||
int transactionId,
|
||||
List<TransactionLineUpdateInput> lines,
|
||||
) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
'$_basePath/$transactionId/lines',
|
||||
data: {
|
||||
'id': transactionId,
|
||||
'lines': lines.map((line) => line.toJson()).toList(growable: false),
|
||||
},
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
return _parseLines(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteLine(int lineId) async {
|
||||
await _api.delete<void>('$_linePath/$lineId');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransactionLine> restoreLine(int lineId) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
'$_linePath/$lineId/restore',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final lines = _parseLines(response.data);
|
||||
if (lines.isEmpty) {
|
||||
throw StateError('복구된 라인 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
return lines.first;
|
||||
}
|
||||
|
||||
List<StockTransactionLine> _parseLines(Map<String, dynamic>? body) {
|
||||
final data = _extractData(body);
|
||||
if (data['lines'] is List) {
|
||||
final dto = StockTransactionDto.fromJson(data);
|
||||
return dto.lines;
|
||||
}
|
||||
if (data.containsKey('id')) {
|
||||
final dto = StockTransactionDto.fromJson({
|
||||
'lines': [data],
|
||||
});
|
||||
return dto.lines;
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractData(Map<String, dynamic>? body) {
|
||||
if (body == null) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
if (body['data'] is Map<String, dynamic>) {
|
||||
return body['data'] as Map<String, dynamic>;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/// 재고 트랜잭션 생성 입력 모델.
|
||||
class StockTransactionCreateInput {
|
||||
StockTransactionCreateInput({
|
||||
this.transactionNo,
|
||||
required this.transactionTypeId,
|
||||
required this.transactionStatusId,
|
||||
required this.warehouseId,
|
||||
required this.transactionDate,
|
||||
required this.createdById,
|
||||
this.note,
|
||||
this.expectedReturnDate,
|
||||
this.lines = const [],
|
||||
this.customers = const [],
|
||||
});
|
||||
|
||||
final String? transactionNo;
|
||||
final int transactionTypeId;
|
||||
final int transactionStatusId;
|
||||
final int warehouseId;
|
||||
final DateTime transactionDate;
|
||||
final int createdById;
|
||||
final String? note;
|
||||
final DateTime? expectedReturnDate;
|
||||
final List<TransactionLineCreateInput> lines;
|
||||
final List<TransactionCustomerCreateInput> customers;
|
||||
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
if (transactionNo != null && transactionNo!.trim().isNotEmpty)
|
||||
'transaction_no': transactionNo,
|
||||
'transaction_type_id': transactionTypeId,
|
||||
'transaction_status_id': transactionStatusId,
|
||||
'warehouse_id': warehouseId,
|
||||
'transaction_date': transactionDate.toIso8601String(),
|
||||
'created_by_id': createdById,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
if (expectedReturnDate != null)
|
||||
'expected_return_date': expectedReturnDate!.toIso8601String(),
|
||||
if (lines.isNotEmpty)
|
||||
'lines': lines.map((line) => line.toJson()).toList(growable: false),
|
||||
if (customers.isNotEmpty)
|
||||
'customers': customers
|
||||
.map((customer) => customer.toJson())
|
||||
.toList(growable: false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 수정 입력 모델.
|
||||
class StockTransactionUpdateInput {
|
||||
StockTransactionUpdateInput({
|
||||
required this.transactionStatusId,
|
||||
this.note,
|
||||
this.expectedReturnDate,
|
||||
});
|
||||
|
||||
final int transactionStatusId;
|
||||
final String? note;
|
||||
final DateTime? expectedReturnDate;
|
||||
|
||||
Map<String, dynamic> toPayload() {
|
||||
return {
|
||||
'transaction_status_id': transactionStatusId,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
if (expectedReturnDate != null)
|
||||
'expected_return_date': expectedReturnDate!.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 라인 추가 입력 모델.
|
||||
class TransactionLineCreateInput {
|
||||
TransactionLineCreateInput({
|
||||
required this.lineNo,
|
||||
required this.productId,
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final int lineNo;
|
||||
final int productId;
|
||||
final int quantity;
|
||||
final double unitPrice;
|
||||
final String? note;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'line_no': lineNo,
|
||||
'product_id': productId,
|
||||
'quantity': quantity,
|
||||
'unit_price': unitPrice,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 라인 수정 입력 모델.
|
||||
class TransactionLineUpdateInput {
|
||||
TransactionLineUpdateInput({
|
||||
required this.id,
|
||||
this.lineNo,
|
||||
this.quantity,
|
||||
this.unitPrice,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int? lineNo;
|
||||
final int? quantity;
|
||||
final double? unitPrice;
|
||||
final String? note;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
if (lineNo != null) 'line_no': lineNo,
|
||||
if (quantity != null) 'quantity': quantity,
|
||||
if (unitPrice != null) 'unit_price': unitPrice,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 고객 연결 추가 입력 모델.
|
||||
class TransactionCustomerCreateInput {
|
||||
TransactionCustomerCreateInput({required this.customerId, this.note});
|
||||
|
||||
final int customerId;
|
||||
final String? note;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'customer_id': customerId,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 고객 연결 수정 입력 모델.
|
||||
class TransactionCustomerUpdateInput {
|
||||
TransactionCustomerUpdateInput({required this.id, this.note});
|
||||
|
||||
final int id;
|
||||
final String? note;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 목록 조회 필터 모델.
|
||||
class StockTransactionListFilter {
|
||||
StockTransactionListFilter({
|
||||
this.page = 1,
|
||||
this.pageSize = 20,
|
||||
this.query,
|
||||
this.transactionTypeId,
|
||||
this.transactionStatusId,
|
||||
this.warehouseId,
|
||||
this.customerId,
|
||||
this.from,
|
||||
this.to,
|
||||
this.sort,
|
||||
this.order,
|
||||
this.include = const ['lines', 'customers', 'approval'],
|
||||
});
|
||||
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final String? query;
|
||||
final int? transactionTypeId;
|
||||
final int? transactionStatusId;
|
||||
final int? warehouseId;
|
||||
final int? customerId;
|
||||
final DateTime? from;
|
||||
final DateTime? to;
|
||||
final String? sort;
|
||||
final String? order;
|
||||
final List<String> include;
|
||||
|
||||
Map<String, dynamic> toQuery() {
|
||||
return {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
if (query != null && query!.trim().isNotEmpty) 'q': query,
|
||||
if (transactionTypeId != null) 'transaction_type_id': transactionTypeId,
|
||||
if (transactionStatusId != null)
|
||||
'transaction_status_id': transactionStatusId,
|
||||
if (warehouseId != null) 'warehouse_id': warehouseId,
|
||||
if (customerId != null) 'customer_id': customerId,
|
||||
if (from != null) 'from': from!.toIso8601String(),
|
||||
if (to != null) 'to': to!.toIso8601String(),
|
||||
if (sort != null && sort!.trim().isNotEmpty) 'sort': sort,
|
||||
if (order != null && order!.trim().isNotEmpty) 'order': order,
|
||||
if (include.isNotEmpty) 'include': include.join(','),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/stock_transaction.dart';
|
||||
import '../entities/stock_transaction_input.dart';
|
||||
|
||||
/// 재고 트랜잭션 저장소 인터페이스.
|
||||
abstract class StockTransactionRepository {
|
||||
/// 재고 트랜잭션 목록을 조회한다.
|
||||
Future<PaginatedResult<StockTransaction>> list({
|
||||
StockTransactionListFilter? filter,
|
||||
});
|
||||
|
||||
/// 재고 트랜잭션 단건을 조회한다.
|
||||
Future<StockTransaction> fetchDetail(
|
||||
int id, {
|
||||
List<String> include = const ['lines', 'customers', 'approval'],
|
||||
});
|
||||
|
||||
/// 재고 트랜잭션을 생성한다.
|
||||
Future<StockTransaction> create(StockTransactionCreateInput input);
|
||||
|
||||
/// 재고 트랜잭션을 수정한다.
|
||||
Future<StockTransaction> update(int id, StockTransactionUpdateInput input);
|
||||
|
||||
/// 재고 트랜잭션을 삭제한다.
|
||||
Future<void> delete(int id);
|
||||
|
||||
/// 삭제된 재고 트랜잭션을 복구한다.
|
||||
Future<StockTransaction> restore(int id);
|
||||
|
||||
/// 재고 트랜잭션을 상신(submit)한다.
|
||||
Future<StockTransaction> submit(int id);
|
||||
|
||||
/// 재고 트랜잭션을 완료 처리한다.
|
||||
Future<StockTransaction> complete(int id);
|
||||
|
||||
/// 재고 트랜잭션을 승인 처리한다.
|
||||
Future<StockTransaction> approve(int id);
|
||||
|
||||
/// 재고 트랜잭션을 반려 처리한다.
|
||||
Future<StockTransaction> reject(int id);
|
||||
|
||||
/// 재고 트랜잭션을 취소 처리한다.
|
||||
Future<StockTransaction> cancel(int id);
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 라인 저장소 인터페이스.
|
||||
abstract class TransactionLineRepository {
|
||||
/// 라인을 추가한다.
|
||||
Future<List<StockTransactionLine>> addLines(
|
||||
int transactionId,
|
||||
List<TransactionLineCreateInput> lines,
|
||||
);
|
||||
|
||||
/// 라인 정보를 일괄 수정한다.
|
||||
Future<List<StockTransactionLine>> updateLines(
|
||||
int transactionId,
|
||||
List<TransactionLineUpdateInput> lines,
|
||||
);
|
||||
|
||||
/// 단일 라인을 삭제한다.
|
||||
Future<void> deleteLine(int lineId);
|
||||
|
||||
/// 삭제된 라인을 복구한다.
|
||||
Future<StockTransactionLine> restoreLine(int lineId);
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 고객 연결 저장소 인터페이스.
|
||||
abstract class TransactionCustomerRepository {
|
||||
/// 고객 연결을 추가한다.
|
||||
Future<List<StockTransactionCustomer>> addCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerCreateInput> customers,
|
||||
);
|
||||
|
||||
/// 고객 연결 정보를 수정한다.
|
||||
Future<List<StockTransactionCustomer>> updateCustomers(
|
||||
int transactionId,
|
||||
List<TransactionCustomerUpdateInput> customers,
|
||||
);
|
||||
|
||||
/// 고객 연결을 삭제한다.
|
||||
Future<void> deleteCustomer(int customerLinkId);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
/// 라인 편집 폼에서 수집한 입력 값을 정규화한 모델.
|
||||
class TransactionLineDraft {
|
||||
TransactionLineDraft({
|
||||
this.id,
|
||||
required this.lineNo,
|
||||
required this.productId,
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
this.note,
|
||||
});
|
||||
|
||||
/// 기존 라인의 식별자. 신규 라인은 `null`이다.
|
||||
final int? id;
|
||||
|
||||
/// 서버에 전달할 라인 순번.
|
||||
final int lineNo;
|
||||
|
||||
/// 선택된 제품의 식별자.
|
||||
final int productId;
|
||||
|
||||
/// 수량.
|
||||
final int quantity;
|
||||
|
||||
/// 단가.
|
||||
final double unitPrice;
|
||||
|
||||
/// 비고. 공백일 경우 `null`로 정규화한다.
|
||||
final String? note;
|
||||
}
|
||||
|
||||
/// 라인 변경 사항을 서버 호출 단위로 정리한 결과.
|
||||
class TransactionLineSyncPlan {
|
||||
TransactionLineSyncPlan({
|
||||
this.createdLines = const [],
|
||||
this.updatedLines = const [],
|
||||
this.deletedLineIds = const [],
|
||||
});
|
||||
|
||||
/// 새로 추가해야 할 라인 목록.
|
||||
final List<TransactionLineCreateInput> createdLines;
|
||||
|
||||
/// 수정이 필요한 기존 라인 목록.
|
||||
final List<TransactionLineUpdateInput> updatedLines;
|
||||
|
||||
/// 삭제해야 할 라인 ID 목록.
|
||||
final List<int> deletedLineIds;
|
||||
|
||||
/// 변경 사항이 있는지 여부.
|
||||
bool get hasChanges =>
|
||||
createdLines.isNotEmpty ||
|
||||
updatedLines.isNotEmpty ||
|
||||
deletedLineIds.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 고객 연결 입력 값을 정규화한 모델.
|
||||
class TransactionCustomerDraft {
|
||||
TransactionCustomerDraft({this.id, required this.customerId, this.note});
|
||||
|
||||
/// 기존 고객 연결 ID. 신규 연결은 `null`이다.
|
||||
final int? id;
|
||||
|
||||
/// 선택된 고객 ID.
|
||||
final int customerId;
|
||||
|
||||
/// 비고. 공백일 경우 `null`.
|
||||
final String? note;
|
||||
}
|
||||
|
||||
/// 고객 연결 변경 사항을 서버 호출 단위로 정리한 결과.
|
||||
class TransactionCustomerSyncPlan {
|
||||
TransactionCustomerSyncPlan({
|
||||
this.createdCustomers = const [],
|
||||
this.updatedCustomers = const [],
|
||||
this.deletedCustomerIds = const [],
|
||||
});
|
||||
|
||||
/// 새로 추가할 고객 연결 입력.
|
||||
final List<TransactionCustomerCreateInput> createdCustomers;
|
||||
|
||||
/// 비고 등 속성이 변경된 고객 연결 입력.
|
||||
final List<TransactionCustomerUpdateInput> updatedCustomers;
|
||||
|
||||
/// 삭제 대상 고객 연결 ID.
|
||||
final List<int> deletedCustomerIds;
|
||||
|
||||
/// 변경 사항이 있는지 여부.
|
||||
bool get hasChanges =>
|
||||
createdCustomers.isNotEmpty ||
|
||||
updatedCustomers.isNotEmpty ||
|
||||
deletedCustomerIds.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 재고 트랜잭션 라인·고객 입력을 기존 데이터와 비교해 동기화 계획을 산출하는 유틸리티.
|
||||
class TransactionDetailSyncService {
|
||||
const TransactionDetailSyncService();
|
||||
|
||||
/// 현재 서버 상태와 폼 입력을 비교해 라인 변경 계획을 생성한다.
|
||||
TransactionLineSyncPlan buildLinePlan({
|
||||
required List<TransactionLineDraft> drafts,
|
||||
required List<StockTransactionLine> currentLines,
|
||||
}) {
|
||||
final createdLines = <TransactionLineCreateInput>[];
|
||||
final updatedLines = <TransactionLineUpdateInput>[];
|
||||
final deletedLineIds = <int>{};
|
||||
|
||||
final currentById = <int, StockTransactionLine>{};
|
||||
for (final line in currentLines) {
|
||||
final id = line.id;
|
||||
if (id != null) {
|
||||
currentById[id] = line;
|
||||
}
|
||||
}
|
||||
final remainingIds = currentById.keys.toSet();
|
||||
|
||||
for (final draft in drafts) {
|
||||
final draftId = draft.id;
|
||||
final normalizedNote = _normalizeOptionalText(draft.note);
|
||||
if (draftId == null) {
|
||||
createdLines.add(
|
||||
TransactionLineCreateInput(
|
||||
lineNo: draft.lineNo,
|
||||
productId: draft.productId,
|
||||
quantity: draft.quantity,
|
||||
unitPrice: draft.unitPrice,
|
||||
note: normalizedNote,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final current = currentById[draftId];
|
||||
if (current == null) {
|
||||
createdLines.add(
|
||||
TransactionLineCreateInput(
|
||||
lineNo: draft.lineNo,
|
||||
productId: draft.productId,
|
||||
quantity: draft.quantity,
|
||||
unitPrice: draft.unitPrice,
|
||||
note: normalizedNote,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
remainingIds.remove(draftId);
|
||||
|
||||
if (current.product.id != draft.productId) {
|
||||
deletedLineIds.add(draftId);
|
||||
createdLines.add(
|
||||
TransactionLineCreateInput(
|
||||
lineNo: draft.lineNo,
|
||||
productId: draft.productId,
|
||||
quantity: draft.quantity,
|
||||
unitPrice: draft.unitPrice,
|
||||
note: normalizedNote,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
int? nextLineNo;
|
||||
if (current.lineNo != draft.lineNo) {
|
||||
nextLineNo = draft.lineNo;
|
||||
}
|
||||
|
||||
int? nextQuantity;
|
||||
if (current.quantity != draft.quantity) {
|
||||
nextQuantity = draft.quantity;
|
||||
}
|
||||
|
||||
double? nextUnitPrice;
|
||||
if ((current.unitPrice - draft.unitPrice).abs() > 0.0001) {
|
||||
nextUnitPrice = draft.unitPrice;
|
||||
}
|
||||
|
||||
final currentNote = _normalizeOptionalText(current.note);
|
||||
String? nextNote;
|
||||
if (currentNote != normalizedNote) {
|
||||
nextNote = normalizedNote;
|
||||
}
|
||||
|
||||
if (nextLineNo != null ||
|
||||
nextQuantity != null ||
|
||||
nextUnitPrice != null ||
|
||||
nextNote != null) {
|
||||
updatedLines.add(
|
||||
TransactionLineUpdateInput(
|
||||
id: draftId,
|
||||
lineNo: nextLineNo,
|
||||
quantity: nextQuantity,
|
||||
unitPrice: nextUnitPrice,
|
||||
note: nextNote,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final id in remainingIds) {
|
||||
deletedLineIds.add(id);
|
||||
}
|
||||
|
||||
return TransactionLineSyncPlan(
|
||||
createdLines: createdLines,
|
||||
updatedLines: updatedLines,
|
||||
deletedLineIds: deletedLineIds.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 현재 서버 상태와 폼 입력을 비교해 고객 연결 변경 계획을 생성한다.
|
||||
TransactionCustomerSyncPlan buildCustomerPlan({
|
||||
required List<TransactionCustomerDraft> drafts,
|
||||
required List<StockTransactionCustomer> currentCustomers,
|
||||
}) {
|
||||
final createdCustomers = <TransactionCustomerCreateInput>[];
|
||||
final updatedCustomers = <TransactionCustomerUpdateInput>[];
|
||||
final deletedCustomerIds = <int>{};
|
||||
|
||||
final currentById = <int, StockTransactionCustomer>{};
|
||||
final currentByCustomerId = <int, StockTransactionCustomer>{};
|
||||
|
||||
for (final customer in currentCustomers) {
|
||||
final id = customer.id;
|
||||
if (id != null) {
|
||||
currentById[id] = customer;
|
||||
}
|
||||
currentByCustomerId[customer.customer.id] = customer;
|
||||
}
|
||||
final remainingIds = currentById.keys.toSet();
|
||||
|
||||
for (final draft in drafts) {
|
||||
final draftId = draft.id;
|
||||
final normalizedNote = _normalizeOptionalText(draft.note);
|
||||
if (draftId != null) {
|
||||
final currentByLink = currentById[draftId];
|
||||
if (currentByLink != null) {
|
||||
remainingIds.remove(draftId);
|
||||
final currentNote = _normalizeOptionalText(currentByLink.note);
|
||||
if (currentNote != normalizedNote) {
|
||||
updatedCustomers.add(
|
||||
TransactionCustomerUpdateInput(id: draftId, note: normalizedNote),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final fallback = currentByCustomerId[draft.customerId];
|
||||
if (fallback != null && fallback.id != null) {
|
||||
final fallbackId = fallback.id!;
|
||||
remainingIds.remove(fallbackId);
|
||||
final currentNote = _normalizeOptionalText(fallback.note);
|
||||
if (currentNote != normalizedNote) {
|
||||
updatedCustomers.add(
|
||||
TransactionCustomerUpdateInput(
|
||||
id: fallbackId,
|
||||
note: normalizedNote,
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
createdCustomers.add(
|
||||
TransactionCustomerCreateInput(
|
||||
customerId: draft.customerId,
|
||||
note: normalizedNote,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final id in remainingIds) {
|
||||
deletedCustomerIds.add(id);
|
||||
}
|
||||
|
||||
return TransactionCustomerSyncPlan(
|
||||
createdCustomers: createdCustomers,
|
||||
updatedCustomers: updatedCustomers,
|
||||
deletedCustomerIds: deletedCustomerIds.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? _normalizeOptionalText(String? value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
Reference in New Issue
Block a user