refactor: 인벤토리 테이블 스펙과 도메인 계층 정비
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/data/repositories/inventory_lookup_repository_remote.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late InventoryLookupRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = InventoryLookupRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Future<void> stubGet(String path) async {
|
||||
when(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
path,
|
||||
query: any(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'items': [
|
||||
{'id': 1, 'name': '샘플'},
|
||||
],
|
||||
},
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
test('fetchTransactionStatuses는 /transaction-statuses를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-statuses';
|
||||
await stubGet(path);
|
||||
|
||||
final items = await repository.fetchTransactionStatuses();
|
||||
|
||||
expect(items, isNotEmpty);
|
||||
final captured = verify(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
query: captureAny(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured;
|
||||
expect(captured[0], equals(path));
|
||||
final query = captured[1] as Map<String, dynamic>;
|
||||
expect(query['page'], 1);
|
||||
expect(query['page_size'], 200);
|
||||
expect(query['is_active'], true);
|
||||
});
|
||||
|
||||
test('fetchApprovalActions는 is_active 파라미터 없이 호출한다', () async {
|
||||
const path = '/api/v1/approval-actions';
|
||||
await stubGet(path);
|
||||
|
||||
await repository.fetchApprovalActions();
|
||||
|
||||
final query =
|
||||
verify(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
query: captureAny(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
expect(query.containsKey('is_active'), isFalse);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_error.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
import 'package:superport_v2/features/inventory/outbound/presentation/controllers/outbound_controller.dart';
|
||||
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';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
|
||||
class _MockStockTransactionRepository extends Mock
|
||||
implements StockTransactionRepository {}
|
||||
|
||||
class _MockInventoryLookupRepository extends Mock
|
||||
implements InventoryLookupRepository {}
|
||||
|
||||
class _MockTransactionLineRepository extends Mock
|
||||
implements TransactionLineRepository {}
|
||||
|
||||
class _MockTransactionCustomerRepository extends Mock
|
||||
implements TransactionCustomerRepository {}
|
||||
|
||||
class _FakeStockTransactionCreateInput extends Fake
|
||||
implements StockTransactionCreateInput {}
|
||||
|
||||
class _FakeStockTransactionUpdateInput extends Fake
|
||||
implements StockTransactionUpdateInput {}
|
||||
|
||||
class _FakeStockTransactionListFilter extends Fake
|
||||
implements StockTransactionListFilter {}
|
||||
|
||||
void main() {
|
||||
group('OutboundController', () {
|
||||
late StockTransactionRepository transactionRepository;
|
||||
late InventoryLookupRepository lookupRepository;
|
||||
late TransactionLineRepository lineRepository;
|
||||
late TransactionCustomerRepository customerRepository;
|
||||
late OutboundController controller;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeStockTransactionCreateInput());
|
||||
registerFallbackValue(_FakeStockTransactionUpdateInput());
|
||||
registerFallbackValue(_FakeStockTransactionListFilter());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
transactionRepository = _MockStockTransactionRepository();
|
||||
lookupRepository = _MockInventoryLookupRepository();
|
||||
lineRepository = _MockTransactionLineRepository();
|
||||
customerRepository = _MockTransactionCustomerRepository();
|
||||
controller = OutboundController(
|
||||
transactionRepository: transactionRepository,
|
||||
lineRepository: lineRepository,
|
||||
customerRepository: customerRepository,
|
||||
lookupRepository: lookupRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('createTransaction은 레코드를 추가한다', () async {
|
||||
final transaction = _buildTransaction();
|
||||
when(
|
||||
() => transactionRepository.create(any()),
|
||||
).thenAnswer((_) async => transaction);
|
||||
|
||||
final record = await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionTypeId: 1,
|
||||
transactionStatusId: 2,
|
||||
warehouseId: 3,
|
||||
transactionDate: DateTime(2024, 4, 1),
|
||||
createdById: 7,
|
||||
),
|
||||
refreshAfter: false,
|
||||
);
|
||||
|
||||
expect(record.id, equals(transaction.id));
|
||||
expect(controller.records.first.id, equals(transaction.id));
|
||||
});
|
||||
|
||||
test('completeTransaction은 레코드를 갱신하고 처리 상태를 추적한다', () async {
|
||||
final original = _buildTransaction();
|
||||
final completed = _buildTransaction(statusName: '출고완료');
|
||||
when(
|
||||
() => transactionRepository.create(any()),
|
||||
).thenAnswer((_) async => original);
|
||||
when(
|
||||
() => transactionRepository.complete(any()),
|
||||
).thenAnswer((_) async => completed);
|
||||
|
||||
await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionTypeId: 1,
|
||||
transactionStatusId: 2,
|
||||
warehouseId: 3,
|
||||
transactionDate: DateTime(2024, 4, 1),
|
||||
createdById: 7,
|
||||
),
|
||||
refreshAfter: false,
|
||||
);
|
||||
|
||||
final future = controller.completeTransaction(
|
||||
original.id!,
|
||||
refreshAfter: false,
|
||||
);
|
||||
|
||||
expect(controller.processingTransactionIds.contains(original.id), isTrue);
|
||||
|
||||
final record = await future;
|
||||
|
||||
expect(record.status, equals('출고완료'));
|
||||
expect(controller.records.first.status, equals('출고완료'));
|
||||
expect(
|
||||
controller.processingTransactionIds.contains(original.id),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchTransactions 실패 시 Failure 메시지를 기록한다', () async {
|
||||
final exception = ApiException(
|
||||
code: ApiErrorCode.badRequest,
|
||||
message: '출고 데이터를 가져오지 못했습니다.',
|
||||
details: {
|
||||
'errors': {
|
||||
'warehouse_id': ['창고를 선택하세요.'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
when(
|
||||
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||
).thenThrow(exception);
|
||||
|
||||
await controller.fetchTransactions(
|
||||
filter: StockTransactionListFilter(transactionTypeId: 1),
|
||||
);
|
||||
|
||||
final failure = Failure.from(exception);
|
||||
expect(controller.errorMessage, equals(failure.describe()));
|
||||
expect(controller.records, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
StockTransaction _buildTransaction({int id = 200, String statusName = '출고대기'}) {
|
||||
return StockTransaction(
|
||||
id: id,
|
||||
transactionNo: 'OUT-$id',
|
||||
transactionDate: DateTime(2024, 4, 1),
|
||||
type: StockTransactionType(id: 20, name: '출고'),
|
||||
status: StockTransactionStatus(id: 21, name: statusName),
|
||||
warehouse: StockTransactionWarehouse(id: 2, code: 'WH-2', name: '부산 센터'),
|
||||
createdBy: StockTransactionEmployee(
|
||||
id: 2,
|
||||
employeeNo: 'EMP-2',
|
||||
name: '출고 담당',
|
||||
),
|
||||
lines: [
|
||||
StockTransactionLine(
|
||||
id: 10,
|
||||
lineNo: 1,
|
||||
product: StockTransactionProduct(id: 10, code: 'P-10', name: '출고 품목'),
|
||||
quantity: 3,
|
||||
unitPrice: 2500.0,
|
||||
),
|
||||
],
|
||||
customers: [
|
||||
StockTransactionCustomer(
|
||||
id: 1,
|
||||
customer: StockTransactionCustomerSummary(
|
||||
id: 100,
|
||||
code: 'C-1',
|
||||
name: '슈퍼포트 고객',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/network/api_error.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
import 'package:superport_v2/features/inventory/rental/presentation/controllers/rental_controller.dart';
|
||||
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';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
|
||||
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
|
||||
class _MockStockTransactionRepository extends Mock
|
||||
implements StockTransactionRepository {}
|
||||
|
||||
class _MockInventoryLookupRepository extends Mock
|
||||
implements InventoryLookupRepository {}
|
||||
|
||||
class _MockTransactionLineRepository extends Mock
|
||||
implements TransactionLineRepository {}
|
||||
|
||||
class _MockTransactionCustomerRepository extends Mock
|
||||
implements TransactionCustomerRepository {}
|
||||
|
||||
class _FakeStockTransactionCreateInput extends Fake
|
||||
implements StockTransactionCreateInput {}
|
||||
|
||||
class _FakeStockTransactionUpdateInput extends Fake
|
||||
implements StockTransactionUpdateInput {}
|
||||
|
||||
class _FakeStockTransactionListFilter extends Fake
|
||||
implements StockTransactionListFilter {}
|
||||
|
||||
void main() {
|
||||
group('RentalController', () {
|
||||
late StockTransactionRepository transactionRepository;
|
||||
late InventoryLookupRepository lookupRepository;
|
||||
late TransactionLineRepository lineRepository;
|
||||
late TransactionCustomerRepository customerRepository;
|
||||
late RentalController controller;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeStockTransactionCreateInput());
|
||||
registerFallbackValue(_FakeStockTransactionUpdateInput());
|
||||
registerFallbackValue(_FakeStockTransactionListFilter());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
transactionRepository = _MockStockTransactionRepository();
|
||||
lookupRepository = _MockInventoryLookupRepository();
|
||||
lineRepository = _MockTransactionLineRepository();
|
||||
customerRepository = _MockTransactionCustomerRepository();
|
||||
controller = RentalController(
|
||||
transactionRepository: transactionRepository,
|
||||
lineRepository: lineRepository,
|
||||
customerRepository: customerRepository,
|
||||
lookupRepository: lookupRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('createTransaction은 레코드를 추가한다', () async {
|
||||
final transaction = _buildRentalTransaction();
|
||||
when(
|
||||
() => transactionRepository.create(any()),
|
||||
).thenAnswer((_) async => transaction);
|
||||
|
||||
final record = await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionTypeId: 1,
|
||||
transactionStatusId: 2,
|
||||
warehouseId: 4,
|
||||
transactionDate: DateTime(2024, 5, 1),
|
||||
createdById: 5,
|
||||
),
|
||||
refreshAfter: false,
|
||||
);
|
||||
|
||||
expect(record.id, equals(transaction.id));
|
||||
expect(controller.records.first.id, equals(transaction.id));
|
||||
});
|
||||
|
||||
test('deleteTransaction은 레코드를 제거하고 처리 상태를 초기화한다', () async {
|
||||
final transaction = _buildRentalTransaction();
|
||||
when(
|
||||
() => transactionRepository.create(any()),
|
||||
).thenAnswer((_) async => transaction);
|
||||
when(
|
||||
() => transactionRepository.delete(any()),
|
||||
).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
await controller.createTransaction(
|
||||
StockTransactionCreateInput(
|
||||
transactionTypeId: 1,
|
||||
transactionStatusId: 2,
|
||||
warehouseId: 4,
|
||||
transactionDate: DateTime(2024, 5, 1),
|
||||
createdById: 5,
|
||||
),
|
||||
refreshAfter: false,
|
||||
);
|
||||
|
||||
final future = controller.deleteTransaction(
|
||||
transaction.id!,
|
||||
refreshAfter: false,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.processingTransactionIds.contains(transaction.id),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
await future;
|
||||
|
||||
expect(controller.records, isEmpty);
|
||||
expect(
|
||||
controller.processingTransactionIds.contains(transaction.id),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('completeTransaction은 마지막 필터 설정을 유지하며 새로고침한다', () async {
|
||||
final rental = _buildRentalTransaction();
|
||||
final other = _buildNonRentalTransaction();
|
||||
|
||||
when(
|
||||
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<StockTransaction>(
|
||||
items: [rental, other],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 2,
|
||||
),
|
||||
);
|
||||
|
||||
await controller.fetchTransactions(
|
||||
filter: StockTransactionListFilter(transactionTypeId: 1),
|
||||
filterByRentalTypes: false,
|
||||
);
|
||||
|
||||
final updated = rental.copyWith(
|
||||
status: StockTransactionStatus(id: 2, name: '완료'),
|
||||
);
|
||||
when(
|
||||
() => transactionRepository.complete(any()),
|
||||
).thenAnswer((_) async => updated);
|
||||
when(
|
||||
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<StockTransaction>(
|
||||
items: [updated, other],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 2,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await controller.completeTransaction(rental.id!);
|
||||
|
||||
expect(result.status, equals('완료'));
|
||||
expect(controller.records.length, equals(2));
|
||||
});
|
||||
|
||||
test('fetchTransactions 실패 시 Failure 메시지를 저장한다', () async {
|
||||
final exception = ApiException(
|
||||
code: ApiErrorCode.conflict,
|
||||
message: '대여 목록을 가져오는 데 실패했습니다.',
|
||||
details: {
|
||||
'errors': {
|
||||
'status': ['상태 필터가 올바르지 않습니다.'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
when(
|
||||
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||
).thenThrow(exception);
|
||||
|
||||
await controller.fetchTransactions(
|
||||
filter: StockTransactionListFilter(transactionTypeId: 1),
|
||||
);
|
||||
|
||||
final failure = Failure.from(exception);
|
||||
expect(controller.errorMessage, equals(failure.describe()));
|
||||
expect(controller.records, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
StockTransaction _buildRentalTransaction({
|
||||
int id = 300,
|
||||
String statusName = '대여중',
|
||||
}) {
|
||||
return StockTransaction(
|
||||
id: id,
|
||||
transactionNo: 'RENT-$id',
|
||||
transactionDate: DateTime(2024, 5, 1),
|
||||
type: StockTransactionType(id: 30, name: '대여'),
|
||||
status: StockTransactionStatus(id: 31, name: statusName),
|
||||
warehouse: StockTransactionWarehouse(id: 5, code: 'WH-5', name: '대여 창고'),
|
||||
createdBy: StockTransactionEmployee(
|
||||
id: 5,
|
||||
employeeNo: 'EMP-5',
|
||||
name: '대여 담당',
|
||||
),
|
||||
note: '렌탈',
|
||||
lines: [
|
||||
StockTransactionLine(
|
||||
id: 20,
|
||||
lineNo: 1,
|
||||
product: StockTransactionProduct(id: 20, code: 'P-20', name: '렌탈 품목'),
|
||||
quantity: 2,
|
||||
unitPrice: 15000.0,
|
||||
),
|
||||
],
|
||||
customers: [
|
||||
StockTransactionCustomer(
|
||||
id: 2,
|
||||
customer: StockTransactionCustomerSummary(
|
||||
id: 200,
|
||||
code: 'C-200',
|
||||
name: '렌탈 고객',
|
||||
),
|
||||
),
|
||||
],
|
||||
expectedReturnDate: DateTime(2024, 5, 20),
|
||||
);
|
||||
}
|
||||
|
||||
StockTransaction _buildNonRentalTransaction() {
|
||||
return StockTransaction(
|
||||
id: 301,
|
||||
transactionNo: 'SRV-301',
|
||||
transactionDate: DateTime(2024, 5, 2),
|
||||
type: StockTransactionType(id: 40, name: '수리'),
|
||||
status: StockTransactionStatus(id: 32, name: '진행중'),
|
||||
warehouse: StockTransactionWarehouse(id: 6, code: 'WH-6', name: '서비스 센터'),
|
||||
createdBy: StockTransactionEmployee(
|
||||
id: 6,
|
||||
employeeNo: 'EMP-6',
|
||||
name: '서비스 담당',
|
||||
),
|
||||
lines: [
|
||||
StockTransactionLine(
|
||||
id: 21,
|
||||
lineNo: 1,
|
||||
product: StockTransactionProduct(id: 21, code: 'P-21', name: '서비스 품목'),
|
||||
quantity: 1,
|
||||
unitPrice: 5000.0,
|
||||
),
|
||||
],
|
||||
customers: const [],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_error.dart';
|
||||
import 'package:superport_v2/core/network/failure.dart';
|
||||
import 'package:superport_v2/features/inventory/shared/widgets/warehouse_select_field.dart';
|
||||
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
|
||||
class _MockWarehouseRepository extends Mock implements WarehouseRepository {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
setUp(() async {
|
||||
await getIt.reset();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await getIt.reset();
|
||||
});
|
||||
|
||||
testWidgets('창고 로드 실패 시 Failure 메시지를 표시한다', (tester) async {
|
||||
final repository = _MockWarehouseRepository();
|
||||
getIt.registerSingleton<WarehouseRepository>(repository);
|
||||
|
||||
final exception = ApiException(
|
||||
code: ApiErrorCode.unknown,
|
||||
message: '창고 목록을 불러오지 못했습니다.',
|
||||
details: {
|
||||
'errors': {
|
||||
'warehouse': ['창고 데이터를 조회할 수 없습니다.'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
when(
|
||||
() => repository.list(
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
isActive: true,
|
||||
includeZipcode: false,
|
||||
),
|
||||
).thenThrow(exception);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ShadTheme(
|
||||
data: ShadThemeData(
|
||||
colorScheme: const ShadSlateColorScheme.light(),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: InventoryWarehouseSelectField(onChanged: (_) {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final failure = Failure.from(exception);
|
||||
expect(find.text(failure.describe()), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/data/repositories/stock_transaction_repository_remote.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late StockTransactionRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = StockTransactionRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Future<Response<Map<String, dynamic>>> emptyListResponse(String path) async {
|
||||
return Response<Map<String, dynamic>>(
|
||||
data: {'items': const [], 'page': 1, 'page_size': 20, 'total': 0},
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> detailBody() {
|
||||
return {
|
||||
'data': {
|
||||
'id': 10,
|
||||
'transaction_no': 'TX-1',
|
||||
'transaction_type': {'id': 1, 'type_name': '입고'},
|
||||
'transaction_status': {'id': 1, 'status_name': '작성중'},
|
||||
'warehouse': {'id': 3, 'warehouse_code': 'W-1', 'warehouse_name': '서울'},
|
||||
'created_by': {'id': 5, 'employee_no': 'EMP-1', 'employee_name': '홍길동'},
|
||||
'lines': const [],
|
||||
'customers': const [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('list 호출 시 필터 파라미터를 전달한다', () async {
|
||||
const path = '/api/v1/stock-transactions';
|
||||
when(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
path,
|
||||
query: any(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer((_) => emptyListResponse(path));
|
||||
|
||||
final filter = StockTransactionListFilter(
|
||||
page: 2,
|
||||
pageSize: 50,
|
||||
query: '품번',
|
||||
transactionTypeId: 11,
|
||||
transactionStatusId: 7,
|
||||
warehouseId: 3,
|
||||
customerId: 99,
|
||||
from: DateTime(2024, 1, 1),
|
||||
to: DateTime(2024, 1, 31),
|
||||
sort: 'transaction_date',
|
||||
order: 'desc',
|
||||
include: const ['lines', 'approval'],
|
||||
);
|
||||
|
||||
await repository.list(filter: filter);
|
||||
|
||||
final captured = verify(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
query: captureAny(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured;
|
||||
|
||||
final query = captured[1] as Map<String, dynamic>;
|
||||
expect(captured.first, equals(path));
|
||||
expect(query['page'], 2);
|
||||
expect(query['page_size'], 50);
|
||||
expect(query['q'], '품번');
|
||||
expect(query['transaction_type_id'], 11);
|
||||
expect(query['transaction_status_id'], 7);
|
||||
expect(query['warehouse_id'], 3);
|
||||
expect(query['customer_id'], 99);
|
||||
expect(query['from'], '2024-01-01T00:00:00.000');
|
||||
expect(query['to'], '2024-01-31T00:00:00.000');
|
||||
expect(query['sort'], 'transaction_date');
|
||||
expect(query['order'], 'desc');
|
||||
expect(query['include'], 'lines,approval');
|
||||
});
|
||||
|
||||
test('fetchDetail은 include 파라미터를 조인해 전달한다', () async {
|
||||
const path = '/api/v1/stock-transactions/10';
|
||||
when(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
path,
|
||||
query: any(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.fetchDetail(10, include: const ['lines', 'customers']);
|
||||
|
||||
final query =
|
||||
verify(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
query: captureAny(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(query['include'], 'lines,customers');
|
||||
});
|
||||
|
||||
test('create는 입력 payload를 body로 전달한다', () async {
|
||||
const path = '/api/v1/stock-transactions';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 201,
|
||||
),
|
||||
);
|
||||
|
||||
final input = StockTransactionCreateInput(
|
||||
transactionTypeId: 1,
|
||||
transactionStatusId: 2,
|
||||
warehouseId: 3,
|
||||
transactionDate: DateTime(2024, 2, 1),
|
||||
createdById: 9,
|
||||
note: '테스트',
|
||||
lines: [
|
||||
TransactionLineCreateInput(
|
||||
lineNo: 1,
|
||||
productId: 11,
|
||||
quantity: 2,
|
||||
unitPrice: 1000,
|
||||
),
|
||||
],
|
||||
customers: [TransactionCustomerCreateInput(customerId: 7)],
|
||||
);
|
||||
|
||||
await repository.create(input);
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(payload['transaction_type_id'], 1);
|
||||
expect(payload['transaction_status_id'], 2);
|
||||
expect(payload['warehouse_id'], 3);
|
||||
expect(payload['created_by_id'], 9);
|
||||
expect(payload['lines'], isA<List>());
|
||||
expect(payload['customers'], isA<List>());
|
||||
});
|
||||
|
||||
test('submit은 /submit 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/10/submit';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.submit(10);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('complete는 /complete 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/10/complete';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.complete(10);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('approve는 /approve 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/11/approve';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.approve(11);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('reject는 /reject 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/12/reject';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.reject(12);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('cancel은 /cancel 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/13/cancel';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.cancel(13);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/data/repositories/transaction_customer_repository_remote.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late TransactionCustomerRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = TransactionCustomerRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Map<String, dynamic> customerResponse() {
|
||||
return {
|
||||
'data': {
|
||||
'customers': [
|
||||
{
|
||||
'id': 301,
|
||||
'customer': {
|
||||
'id': 700,
|
||||
'customer_code': 'C-1',
|
||||
'customer_name': '슈퍼포트',
|
||||
},
|
||||
'note': '테스트',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('addCustomers는 거래 ID를 포함해 POST 요청을 보낸다', () async {
|
||||
const path = '/api/v1/stock-transactions/77/customers';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: customerResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.addCustomers(77, [
|
||||
TransactionCustomerCreateInput(customerId: 700, note: '비고'),
|
||||
]);
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(payload['id'], 77);
|
||||
expect(payload['customers'], isA<List>());
|
||||
});
|
||||
|
||||
test('updateCustomers는 PATCH 요청을 보낸다', () async {
|
||||
const path = '/api/v1/stock-transactions/77/customers';
|
||||
when(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: customerResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.updateCustomers(77, [
|
||||
TransactionCustomerUpdateInput(id: 301, note: '수정'),
|
||||
]);
|
||||
|
||||
verify(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('deleteCustomer는 /transaction-customers/{id} 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-customers/301';
|
||||
when(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<void>(
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 204,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.deleteCustomer(301);
|
||||
|
||||
verify(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/data/repositories/transaction_line_repository_remote.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late TransactionLineRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = TransactionLineRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Map<String, dynamic> lineResponse() {
|
||||
return {
|
||||
'data': {
|
||||
'lines': [
|
||||
{
|
||||
'id': 101,
|
||||
'line_no': 1,
|
||||
'product': {'id': 11, 'product_code': 'P-1', 'product_name': '품목'},
|
||||
'quantity': 3,
|
||||
'unit_price': 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('addLines는 거래 ID를 포함한 POST 요청을 보낸다', () async {
|
||||
const path = '/api/v1/stock-transactions/50/lines';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: lineResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.addLines(50, [
|
||||
TransactionLineCreateInput(
|
||||
lineNo: 2,
|
||||
productId: 44,
|
||||
quantity: 5,
|
||||
unitPrice: 1500,
|
||||
),
|
||||
]);
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(payload['id'], 50);
|
||||
expect(payload['lines'], isA<List>());
|
||||
});
|
||||
|
||||
test('updateLines는 PATCH 요청을 사용한다', () async {
|
||||
const path = '/api/v1/stock-transactions/50/lines';
|
||||
when(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: lineResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.updateLines(50, [
|
||||
TransactionLineUpdateInput(id: 101, quantity: 10),
|
||||
]);
|
||||
|
||||
verify(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('deleteLine은 /transaction-lines/{id}를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-lines/101';
|
||||
when(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<void>(
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 204,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.deleteLine(101);
|
||||
|
||||
verify(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('restoreLine은 복구 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-lines/101/restore';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 101,
|
||||
'line_no': 1,
|
||||
'product': {'id': 11, 'product_code': 'P-1', 'product_name': '품목'},
|
||||
'quantity': 3,
|
||||
'unit_price': 1000,
|
||||
},
|
||||
},
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
final line = await repository.restoreLine(101);
|
||||
|
||||
expect(line.id, 101);
|
||||
expect(line.lineNo, 1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/presentation/services/transaction_detail_sync_service.dart';
|
||||
|
||||
StockTransactionLine _buildLine({
|
||||
int? id,
|
||||
int productId = 1,
|
||||
int lineNo = 1,
|
||||
int quantity = 1,
|
||||
double unitPrice = 1000,
|
||||
String? note,
|
||||
}) {
|
||||
return StockTransactionLine(
|
||||
id: id,
|
||||
lineNo: lineNo,
|
||||
product: StockTransactionProduct(
|
||||
id: productId,
|
||||
code: 'P$productId',
|
||||
name: '제품$productId',
|
||||
),
|
||||
quantity: quantity,
|
||||
unitPrice: unitPrice,
|
||||
note: note,
|
||||
);
|
||||
}
|
||||
|
||||
StockTransactionCustomer _buildCustomer({
|
||||
int? id,
|
||||
int customerId = 1,
|
||||
String code = 'C1',
|
||||
String name = '고객1',
|
||||
String? note,
|
||||
}) {
|
||||
return StockTransactionCustomer(
|
||||
id: id,
|
||||
customer: StockTransactionCustomerSummary(
|
||||
id: customerId,
|
||||
code: code,
|
||||
name: name,
|
||||
),
|
||||
note: note,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
const service = TransactionDetailSyncService();
|
||||
|
||||
group('TransactionDetailSyncService', () {
|
||||
test('buildLinePlan는 생성/수정/삭제 계획을 정확히 산출한다', () {
|
||||
final currentLines = [
|
||||
_buildLine(
|
||||
id: 10,
|
||||
productId: 1,
|
||||
lineNo: 1,
|
||||
quantity: 2,
|
||||
unitPrice: 1000,
|
||||
),
|
||||
_buildLine(
|
||||
id: 11,
|
||||
productId: 2,
|
||||
lineNo: 2,
|
||||
quantity: 4,
|
||||
unitPrice: 2000,
|
||||
),
|
||||
_buildLine(
|
||||
id: 12,
|
||||
productId: 3,
|
||||
lineNo: 3,
|
||||
quantity: 6,
|
||||
unitPrice: 3000,
|
||||
),
|
||||
];
|
||||
|
||||
final drafts = [
|
||||
TransactionLineDraft(
|
||||
id: 10,
|
||||
lineNo: 1,
|
||||
productId: 1,
|
||||
quantity: 3,
|
||||
unitPrice: 1000,
|
||||
note: null,
|
||||
),
|
||||
TransactionLineDraft(
|
||||
id: 11,
|
||||
lineNo: 2,
|
||||
productId: 4,
|
||||
quantity: 4,
|
||||
unitPrice: 2200,
|
||||
note: '교체',
|
||||
),
|
||||
TransactionLineDraft(
|
||||
lineNo: 3,
|
||||
productId: 5,
|
||||
quantity: 1,
|
||||
unitPrice: 500,
|
||||
note: null,
|
||||
),
|
||||
];
|
||||
|
||||
final plan = service.buildLinePlan(
|
||||
drafts: drafts,
|
||||
currentLines: currentLines,
|
||||
);
|
||||
|
||||
expect(plan.createdLines.length, 2);
|
||||
final createdProductIds = plan.createdLines.map((line) => line.productId);
|
||||
expect(createdProductIds, containsAll(<int>[4, 5]));
|
||||
|
||||
expect(plan.updatedLines.length, 1);
|
||||
final update = plan.updatedLines.first;
|
||||
expect(update.id, 10);
|
||||
expect(update.quantity, 3);
|
||||
expect(update.unitPrice, isNull);
|
||||
expect(update.note, isNull);
|
||||
|
||||
expect(plan.deletedLineIds.length, 2);
|
||||
expect(plan.deletedLineIds, containsAll(<int>[11, 12]));
|
||||
});
|
||||
|
||||
test('buildCustomerPlan은 고객 추가/수정/삭제를 구분한다', () {
|
||||
final currentCustomers = [
|
||||
_buildCustomer(
|
||||
id: 20,
|
||||
customerId: 100,
|
||||
code: 'C100',
|
||||
name: '고객 100',
|
||||
note: '메모',
|
||||
),
|
||||
_buildCustomer(id: 21, customerId: 200, code: 'C200', name: '고객 200'),
|
||||
];
|
||||
|
||||
final drafts = [
|
||||
TransactionCustomerDraft(id: 20, customerId: 100, note: '메모 수정'),
|
||||
TransactionCustomerDraft(customerId: 300, note: null),
|
||||
];
|
||||
|
||||
final plan = service.buildCustomerPlan(
|
||||
drafts: drafts,
|
||||
currentCustomers: currentCustomers,
|
||||
);
|
||||
|
||||
expect(plan.createdCustomers.length, 1);
|
||||
expect(plan.createdCustomers.first.customerId, 300);
|
||||
|
||||
expect(plan.updatedCustomers.length, 1);
|
||||
expect(plan.updatedCustomers.first.id, 20);
|
||||
expect(plan.updatedCustomers.first.note, '메모 수정');
|
||||
|
||||
expect(plan.deletedCustomerIds.length, 1);
|
||||
expect(plan.deletedCustomerIds.first, 21);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user