import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:superport_v2/core/common/models/paginated_result.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.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'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); const baseUrl = String.fromEnvironment('STAGING_API_BASE_URL'); const token = String.fromEnvironment('STAGING_API_TOKEN'); const runFlow = bool.fromEnvironment('STAGING_RUN_TRANSACTION_FLOW'); const useFakeFlow = bool.fromEnvironment('STAGING_USE_FAKE_FLOW'); const transactionTypeId = int.fromEnvironment( 'STAGING_TRANSACTION_TYPE_ID', defaultValue: 0, ); const transactionStatusId = int.fromEnvironment( 'STAGING_TRANSACTION_STATUS_ID', defaultValue: 0, ); const warehouseId = int.fromEnvironment( 'STAGING_WAREHOUSE_ID', defaultValue: 0, ); const employeeId = int.fromEnvironment( 'STAGING_EMPLOYEE_ID', defaultValue: 0, ); const productId = int.fromEnvironment('STAGING_PRODUCT_ID', defaultValue: 0); const customerId = int.fromEnvironment( 'STAGING_CUSTOMER_ID', defaultValue: 0, ); final missingConfigs = []; if (!runFlow) { missingConfigs.add('STAGING_RUN_TRANSACTION_FLOW=true'); } if (!useFakeFlow) { if (baseUrl.isEmpty) { missingConfigs.add('STAGING_API_BASE_URL'); } if (token.isEmpty) { missingConfigs.add('STAGING_API_TOKEN'); } if (transactionTypeId == 0) { missingConfigs.add('STAGING_TRANSACTION_TYPE_ID'); } if (transactionStatusId == 0) { missingConfigs.add('STAGING_TRANSACTION_STATUS_ID'); } if (warehouseId == 0) { missingConfigs.add('STAGING_WAREHOUSE_ID'); } if (employeeId == 0) { missingConfigs.add('STAGING_EMPLOYEE_ID'); } if (productId == 0) { missingConfigs.add('STAGING_PRODUCT_ID'); } if (customerId == 0) { missingConfigs.add('STAGING_CUSTOMER_ID'); } } if (missingConfigs.isNotEmpty) { final reason = '환경 변수를 설정하세요: ${missingConfigs.join(', ')}'; testWidgets( 'staging transaction flow (환경 변수 설정 필요: STAGING_RUN_TRANSACTION_FLOW 및 API 식별자)', (tester) async { tester.printToConsole('통합 테스트를 실행하려면 다음 값을 설정하세요.\n$reason'); }, skip: true, ); return; } testWidgets('stock transaction end-to-end flow succeeds', (tester) async { final resolvedTransactionTypeId = transactionTypeId == 0 ? 100 : transactionTypeId; final resolvedTransactionStatusId = transactionStatusId == 0 ? 10 : transactionStatusId; final resolvedWarehouseId = warehouseId == 0 ? 1 : warehouseId; final resolvedEmployeeId = employeeId == 0 ? 1 : employeeId; final resolvedProductId = productId == 0 ? 1 : productId; final resolvedCustomerId = customerId == 0 ? 1 : customerId; late final StockTransactionRepository repository; if (useFakeFlow) { repository = _FakeStockTransactionRepository( transactionTypeId: resolvedTransactionTypeId, initialStatusId: resolvedTransactionStatusId, warehouseId: resolvedWarehouseId, employeeId: resolvedEmployeeId, productId: resolvedProductId, customerId: resolvedCustomerId, ); } else { final dio = Dio( BaseOptions( baseUrl: baseUrl, headers: { 'Authorization': 'Bearer $token', 'Accept': 'application/json', }, ), ); final apiClient = ApiClient(dio: dio); repository = StockTransactionRepositoryRemote(apiClient: apiClient); } final now = DateTime.now(); StockTransactionCreateInput buildCreateInput(String label) { final suffix = '$label-${now.millisecondsSinceEpoch}'; return StockTransactionCreateInput( transactionTypeId: resolvedTransactionTypeId, transactionStatusId: resolvedTransactionStatusId, warehouseId: resolvedWarehouseId, transactionDate: now, createdById: resolvedEmployeeId, note: 'integration-test $suffix', lines: [ TransactionLineCreateInput( lineNo: 1, productId: resolvedProductId, quantity: 1, unitPrice: 1, note: 'line $suffix', ), ], customers: [ TransactionCustomerCreateInput(customerId: resolvedCustomerId), ], approval: StockTransactionApprovalInput( requestedById: resolvedEmployeeId, note: 'approval $suffix', ), ); } bool statusChanged(StockTransaction before, StockTransaction after) { if (before.status.id != after.status.id) { return true; } final beforeName = before.status.name.trim(); final afterName = after.status.name.trim(); return beforeName != afterName; } Future safeDelete(int id) async { try { await repository.delete(id); tester.printToConsole('deleted transaction: $id'); } catch (error) { tester.printToConsole( '삭제 중 경고: transaction $id 제거 실패 (${error.runtimeType})', ); } } final primary = await repository.create(buildCreateInput('primary')); expect(primary.id, isNotNull); final primaryId = primary.id!; tester.printToConsole('created transaction(primary): $primaryId'); final submitted = await repository.submit( primaryId, note: 'integration-submit-primary', ); expect(submitted.id, equals(primaryId)); if (useFakeFlow) { expect(submitted.status.name, contains('승인대기')); } else { expect(statusChanged(primary, submitted), isTrue); } tester.printToConsole( 'submitted transaction: $primaryId (status: ${submitted.status.name})', ); final approved = await repository.approve( primaryId, note: 'integration-approve-primary', ); expect(approved.id, equals(primaryId)); if (useFakeFlow) { expect(approved.status.name, contains('승인완료')); } else { expect(statusChanged(submitted, approved), isTrue); } tester.printToConsole( 'approved transaction: $primaryId (status: ${approved.status.name})', ); final completed = await repository.complete( primaryId, note: 'integration-complete-primary', ); expect(completed.id, equals(primaryId)); if (useFakeFlow) { expect(completed.status.name, contains('완료')); } else { expect(statusChanged(approved, completed), isTrue); } tester.printToConsole( 'completed transaction: $primaryId (status: ${completed.status.name})', ); await safeDelete(primaryId); final cancelTarget = await repository.create(buildCreateInput('cancel')); expect(cancelTarget.id, isNotNull); final cancelId = cancelTarget.id!; tester.printToConsole('created transaction(cancel): $cancelId'); final cancelSubmitted = await repository.submit( cancelId, note: 'integration-submit-cancel', ); expect(cancelSubmitted.id, equals(cancelId)); final cancelled = await repository.cancel( cancelId, note: 'integration-cancel', ); expect(cancelled.id, equals(cancelId)); if (useFakeFlow) { expect(cancelled.status.name, contains('취소')); } else { expect(statusChanged(cancelSubmitted, cancelled), isTrue); } tester.printToConsole( 'cancelled transaction: $cancelId (status: ${cancelled.status.name})', ); await safeDelete(cancelId); final rejectTarget = await repository.create(buildCreateInput('reject')); expect(rejectTarget.id, isNotNull); final rejectId = rejectTarget.id!; tester.printToConsole('created transaction(reject): $rejectId'); final rejectSubmitted = await repository.submit( rejectId, note: 'integration-submit-reject', ); expect(rejectSubmitted.id, equals(rejectId)); final rejected = await repository.reject( rejectId, note: 'integration-reject', ); expect(rejected.id, equals(rejectId)); if (useFakeFlow) { expect(rejected.status.name, contains('반려')); } else { expect(statusChanged(rejectSubmitted, rejected), isTrue); } tester.printToConsole( 'rejected transaction: $rejectId (status: ${rejected.status.name})', ); await safeDelete(rejectId); }); } class _FakeStockTransactionRepository implements StockTransactionRepository { _FakeStockTransactionRepository({ required this.transactionTypeId, required this.initialStatusId, required this.warehouseId, required this.employeeId, required this.productId, required this.customerId, }); final int transactionTypeId; final int initialStatusId; final int warehouseId; final int employeeId; final int productId; final int customerId; int _sequence = 1; final Map _transactions = {}; @override Future create(StockTransactionCreateInput input) async { final id = _sequence++; final generatedNo = 'FAKE-${id.toString().padLeft(6, '0')}'; final transaction = StockTransaction( id: id, transactionNo: generatedNo, transactionDate: input.transactionDate, type: StockTransactionType(id: input.transactionTypeId, name: '테스트 트랜잭션'), status: StockTransactionStatus(id: initialStatusId, name: '작성중'), warehouse: StockTransactionWarehouse( id: warehouseId, code: 'WH-$warehouseId', name: '테스트 창고', ), createdBy: StockTransactionEmployee( id: employeeId, employeeNo: 'EMP-$employeeId', name: '통합 테스트 사용자', ), note: input.note, isActive: true, lines: input.lines .map( (line) => StockTransactionLine( id: line.lineNo, lineNo: line.lineNo, product: StockTransactionProduct( id: line.productId, code: 'P-${line.productId}', name: '테스트 상품', ), quantity: line.quantity, unitPrice: line.unitPrice, note: line.note, ), ) .toList(growable: false), customers: input.customers .map( (customer) => StockTransactionCustomer( id: customer.customerId, customer: StockTransactionCustomerSummary( id: customer.customerId, code: 'C-${customer.customerId}', name: '테스트 고객', ), note: customer.note, ), ) .toList(growable: false), expectedReturnDate: input.expectedReturnDate, ); _transactions[id] = transaction; return transaction; } @override Future submit(int id, {String? note}) async { return _updateStatus( id, StockTransactionStatus(id: initialStatusId + 1, name: '승인대기'), ); } @override Future complete(int id, {String? note}) async { return _updateStatus( id, StockTransactionStatus(id: initialStatusId + 2, name: '완료'), ); } @override Future approve(int id, {String? note}) async { return _updateStatus( id, StockTransactionStatus(id: initialStatusId + 3, name: '승인완료'), ); } @override Future reject(int id, {String? note}) async { return _updateStatus( id, StockTransactionStatus(id: initialStatusId + 4, name: '반려'), ); } @override Future cancel(int id, {String? note}) async { return _updateStatus( id, StockTransactionStatus(id: initialStatusId + 5, name: '취소'), ); } @override Future delete(int id) async { _transactions.remove(id); } @override Future restore(int id) async { return _require(id); } @override Future update( int id, StockTransactionUpdateInput input, ) async { final current = _require(id); final updated = current.copyWith( status: StockTransactionStatus( id: input.transactionStatusId, name: '상태${input.transactionStatusId}', ), note: input.note ?? current.note, expectedReturnDate: input.expectedReturnDate ?? current.expectedReturnDate, ); _transactions[id] = updated; return updated; } @override Future fetchDetail( int id, { List include = const ['lines', 'customers', 'approval'], }) async { return _require(id); } @override Future> list({ StockTransactionListFilter? filter, }) async { final items = _transactions.values.toList(growable: false); return PaginatedResult( items: items, page: filter?.page ?? 1, pageSize: filter?.pageSize ?? items.length, total: items.length, ); } StockTransaction _updateStatus(int id, StockTransactionStatus status) { final current = _require(id); final updated = current.copyWith(status: status); _transactions[id] = updated; return updated; } StockTransaction _require(int id) { final transaction = _transactions[id]; if (transaction == null) { throw StateError('Transaction $id not found'); } return transaction; } }