결재 및 마스터 모듈을 v4 API 계약에 맞게 조정

This commit is contained in:
JiWoong Sul
2025-10-16 17:27:20 +09:00
parent d5c99627db
commit 9e2244f260
34 changed files with 1394 additions and 330 deletions

View File

@@ -197,10 +197,9 @@ class _StubApprovalRepository implements ApprovalRepository {
Future<PaginatedResult<Approval>> list({
int page = 1,
int pageSize = 20,
String? query,
String? status,
DateTime? from,
DateTime? to,
int? transactionId,
int? approvalStatusId,
int? requestedById,
bool includeHistories = false,
bool includeSteps = false,
}) async {

View File

@@ -22,6 +22,52 @@ void main() {
repository = ApprovalRepositoryRemote(apiClient: apiClient);
});
test('list는 신규 필터 파라미터를 전달한다', () async {
const path = '/api/v1/approvals';
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': const [], 'page': 1, 'page_size': 20, 'total': 0},
statusCode: 200,
requestOptions: RequestOptions(path: path),
),
);
await repository.list(
page: 2,
pageSize: 50,
transactionId: 10,
approvalStatusId: 5,
requestedById: 7,
includeSteps: true,
includeHistories: true,
);
final captured = verify(
() => apiClient.get<Map<String, dynamic>>(
captureAny(),
query: captureAny(named: 'query'),
options: any(named: 'options'),
cancelToken: any(named: 'cancelToken'),
),
).captured;
expect(captured.first, equals(path));
final query = captured[1] as Map<String, dynamic>;
expect(query['page'], 2);
expect(query['page_size'], 50);
expect(query['transaction_id'], 10);
expect(query['approval_status_id'], 5);
expect(query['requested_by_id'], 7);
expect(query['include'], 'steps,histories');
});
Map<String, dynamic> buildStep({
required int id,
required int order,
@@ -147,4 +193,52 @@ void main() {
expect(result.histories.length, 2);
expect(result.histories.last.id, 91001);
});
test('fetchDetail은 include 파라미터를 steps,histories로 조합한다', () async {
const path = '/api/v1/approvals/1';
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: {
'data': {
'id': 1,
'approval_no': 'AP-1',
'status': {'id': 1, 'status_name': '대기'},
'current_step': {'id': 2, 'step_order': 1},
'requester': {
'id': 3,
'employee_no': 'EMP-3',
'employee_name': '신청자',
},
'requested_at': '2024-01-01T00:00:00Z',
'steps': const [],
'histories': const [],
},
},
statusCode: 200,
requestOptions: RequestOptions(path: path),
),
);
await repository.fetchDetail(1, includeSteps: true, includeHistories: true);
final query =
verify(
() => apiClient.get<Map<String, dynamic>>(
any(),
query: captureAny(named: 'query'),
options: any(named: 'options'),
cancelToken: any(named: 'cancelToken'),
),
).captured.first
as Map<String, dynamic>;
expect(query['include'], 'steps,histories');
});
}

View File

@@ -8,6 +8,8 @@ import 'package:superport_v2/features/approvals/domain/entities/approval_proceed
import 'package:superport_v2/features/approvals/domain/repositories/approval_repository.dart';
import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart';
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart';
import 'package:superport_v2/features/inventory/lookups/domain/entities/lookup_item.dart';
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
/// ApprovalRepository 모킹 클래스.
class _MockApprovalRepository extends Mock implements ApprovalRepository {}
@@ -26,6 +28,9 @@ class _MockApprovalTemplateRepository extends Mock
class _FakeStepAssignmentInput extends Fake
implements ApprovalStepAssignmentInput {}
class _MockInventoryLookupRepository extends Mock
implements InventoryLookupRepository {}
void main() {
late ApprovalController controller;
late _MockApprovalRepository repository;
@@ -90,10 +95,9 @@ void main() {
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
status: any(named: 'status'),
from: any(named: 'from'),
to: any(named: 'to'),
transactionId: any(named: 'transactionId'),
approvalStatusId: any(named: 'approvalStatusId'),
requestedById: any(named: 'requestedById'),
includeHistories: any(named: 'includeHistories'),
includeSteps: any(named: 'includeSteps'),
),
@@ -110,11 +114,13 @@ void main() {
// 검색어/상태/기간 필터가 Repository 호출에 반영되는지 확인한다.
test('필터 전달을 검증한다', () async {
controller.updateQuery('TRX');
controller.updateTransactionFilter(55);
controller.updateStatusFilter(ApprovalStatusFilter.approved);
final from = DateTime(2024, 4, 1);
final to = DateTime(2024, 4, 30);
controller.updateDateRange(from, to);
controller.updateRequestedByFilter(
id: 77,
name: '상신자',
employeeNo: 'EMP077',
);
await controller.fetch(page: 3);
@@ -122,10 +128,9 @@ void main() {
() => repository.list(
page: 3,
pageSize: 20,
query: 'TRX',
status: 'approved',
from: from,
to: to,
transactionId: 55,
approvalStatusId: null,
requestedById: 77,
includeHistories: false,
includeSteps: false,
),
@@ -138,10 +143,9 @@ void main() {
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
status: any(named: 'status'),
from: any(named: 'from'),
to: any(named: 'to'),
transactionId: any(named: 'transactionId'),
approvalStatusId: any(named: 'approvalStatusId'),
requestedById: any(named: 'requestedById'),
includeHistories: any(named: 'includeHistories'),
includeSteps: any(named: 'includeSteps'),
),
@@ -151,6 +155,46 @@ void main() {
expect(controller.errorMessage, isNotNull);
});
test('상태 룩업 로드 후 status ID를 전달한다', () async {
final lookupRepository = _MockInventoryLookupRepository();
final ctrl = ApprovalController(
approvalRepository: repository,
templateRepository: templateRepository,
lookupRepository: lookupRepository,
);
when(
() => lookupRepository.fetchApprovalStatuses(
activeOnly: any(named: 'activeOnly'),
),
).thenAnswer(
(_) async => [
LookupItem(
id: 5,
name: '승인완료',
code: 'approved',
isDefault: false,
isActive: true,
),
],
);
await ctrl.loadStatusLookups();
ctrl.updateStatusFilter(ApprovalStatusFilter.approved);
await ctrl.fetch();
verify(
() => repository.list(
page: 1,
pageSize: 20,
transactionId: null,
approvalStatusId: 5,
requestedById: null,
includeHistories: false,
includeSteps: false,
),
).called(1);
});
});
group('selectApproval', () {
@@ -304,10 +348,9 @@ void main() {
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
status: any(named: 'status'),
from: any(named: 'from'),
to: any(named: 'to'),
transactionId: any(named: 'transactionId'),
approvalStatusId: any(named: 'approvalStatusId'),
requestedById: any(named: 'requestedById'),
includeHistories: any(named: 'includeHistories'),
includeSteps: any(named: 'includeSteps'),
),
@@ -471,10 +514,9 @@ void main() {
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
status: any(named: 'status'),
from: any(named: 'from'),
to: any(named: 'to'),
transactionId: any(named: 'transactionId'),
approvalStatusId: any(named: 'approvalStatusId'),
requestedById: any(named: 'requestedById'),
includeHistories: any(named: 'includeHistories'),
includeSteps: any(named: 'includeSteps'),
),
@@ -571,15 +613,19 @@ void main() {
});
test('필터 초기화', () {
controller.updateQuery('abc');
controller.updateTransactionFilter(42);
controller.updateStatusFilter(ApprovalStatusFilter.rejected);
controller.updateDateRange(DateTime(2024, 1, 1), DateTime(2024, 1, 31));
controller.updateRequestedByFilter(
id: 11,
name: '요청자',
employeeNo: 'EMP011',
);
controller.clearFilters();
expect(controller.query, isEmpty);
expect(controller.transactionIdFilter, isNull);
expect(controller.statusFilter, ApprovalStatusFilter.all);
expect(controller.fromDate, isNull);
expect(controller.toDate, isNull);
expect(controller.requestedById, isNull);
expect(controller.requestedByName, isNull);
});
}

View File

@@ -105,10 +105,9 @@ void main() {
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
status: any(named: 'status'),
from: any(named: 'from'),
to: any(named: 'to'),
transactionId: any(named: 'transactionId'),
approvalStatusId: any(named: 'approvalStatusId'),
requestedById: any(named: 'requestedById'),
includeHistories: any(named: 'includeHistories'),
includeSteps: any(named: 'includeSteps'),
),

View File

@@ -61,7 +61,7 @@ void main() {
final query = captured[1] as Map<String, dynamic>;
expect(query['page'], 1);
expect(query['page_size'], 200);
expect(query['is_active'], true);
expect(query['active'], true);
});
test('fetchApprovalActions는 is_active 파라미터 없이 호출한다', () async {
@@ -80,6 +80,6 @@ void main() {
),
).captured[1]
as Map<String, dynamic>;
expect(query.containsKey('is_active'), isFalse);
expect(query.containsKey('active'), isFalse);
});
}

View File

@@ -91,8 +91,8 @@ void main() {
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['date_from'], '2024-01-01');
expect(query['date_to'], '2024-01-31');
expect(query['sort'], 'transaction_date');
expect(query['order'], 'desc');
expect(query['include'], 'lines,approval');

View File

@@ -72,7 +72,7 @@ void main() {
expect(query['q'], 'sup');
expect(query['is_partner'], true);
expect(query['is_general'], false);
expect(query['is_active'], true);
expect(query['active'], true);
});
test('fetchDetail은 include=zipcode 파라미터를 전달한다', () async {

View File

@@ -177,7 +177,7 @@ void main() {
await tester.tap(find.text('등록'));
await tester.pump();
expect(find.text('우편번호 검색으로 주소를 선택하세요.'), findsOneWidget);
expect(find.text('검색 버튼을 눌러 주소를 선택하세요.'), findsOneWidget);
});
testWidgets('신규 등록 성공 시 repository.create 호출', (tester) async {
@@ -224,6 +224,7 @@ void main() {
id: 2,
customerCode: capturedInput!.customerCode,
customerName: capturedInput!.customerName,
contactName: capturedInput!.contactName,
isPartner: capturedInput!.isPartner,
isGeneral: capturedInput!.isGeneral,
);
@@ -245,8 +246,9 @@ void main() {
await tester.enterText(editableTexts.at(0), 'C-100');
await tester.enterText(editableTexts.at(1), '신규 고객');
await tester.enterText(editableTexts.at(2), 'new@superport.com');
await tester.enterText(editableTexts.at(3), '02-0000-0000');
await tester.enterText(editableTexts.at(2), '홍길동');
await tester.enterText(editableTexts.at(3), 'new@superport.com');
await tester.enterText(editableTexts.at(4), '02-0000-0000');
// 유형 체크박스: 기본값 partner=false, general=true. partner on 추가
await tester.tap(find.text('파트너'));
@@ -257,6 +259,7 @@ void main() {
expect(capturedInput, isNotNull);
expect(capturedInput?.customerCode, 'C-100');
expect(capturedInput?.contactName, '홍길동');
expect(find.byType(Dialog), findsNothing);
expect(find.text('C-100'), findsOneWidget);
verify(() => repository.create(any())).called(1);

View File

@@ -78,7 +78,7 @@ void main() {
expect(query['include'], 'vendor,uom');
expect(query['vendor_id'], 10);
expect(query['uom_id'], 5);
expect(query['is_active'], false);
expect(query['active'], false);
expect(query['page'], 3);
expect(query['page_size'], 40);
expect(query['q'], 'gear');

View File

@@ -295,7 +295,7 @@ void main() {
await tester.tap(find.text('제조사를 선택하세요'));
await tester.pumpAndSettle();
await tester.tap(find.text('슈퍼벤더'));
await tester.tap(find.text('슈퍼벤더 (V-001)'));
await tester.pumpAndSettle();
await tester.tap(find.text('단위를 선택하세요'));

View File

@@ -98,8 +98,8 @@ void main() {
expect(captured.first, equals(path));
final query = captured[1] as Map<String, dynamic>;
expect(query['from'], request.from.toIso8601String());
expect(query['to'], request.to.toIso8601String());
expect(query['date_from'], '2024-01-01');
expect(query['date_to'], '2024-01-31');
expect(query['format'], 'xlsx');
expect(query['transaction_status_id'], 3);
expect(query['approval_status_id'], 7);