feat(approvals): 결재 접근 차단 대응과 전표 전이 메모 전달 강화
- approvals 모듈에서 APPROVAL_ACCESS_DENIED 응답을 포착하여 ApprovalAccessDeniedException으로 변환하고 접근 거부 시 토스트·대시보드 리다이렉트를 처리 - approval history 조회가 서버 action id에 맞춰 필터링되도록 repository·controller·테스트를 보강 - 재고 트랜잭션 상태 전이 API 호출에 note를 전달하도록 repository·컨트롤러·통합/단위 테스트를 업데이트 - 승인 플로우 QA 체크리스트 및 연동 문서를 최신 계약과 테스트 흐름으로 업데이트
This commit is contained in:
@@ -336,7 +336,7 @@ void main() {
|
||||
|
||||
final updated = _buildTransaction(statusName: '승인대기');
|
||||
when(
|
||||
() => transactionRepository.submit(any()),
|
||||
() => transactionRepository.submit(any(), note: any(named: 'note')),
|
||||
).thenAnswer((_) async => updated);
|
||||
when(
|
||||
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||
@@ -353,7 +353,10 @@ void main() {
|
||||
|
||||
expect(result.status, equals('승인대기'));
|
||||
expect(controller.records.first.status, equals('승인대기'));
|
||||
verify(() => transactionRepository.submit(initial.id!)).called(1);
|
||||
verify(
|
||||
() =>
|
||||
transactionRepository.submit(initial.id!, note: any(named: 'note')),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('fetchTransactions 실패 시 Failure 메시지를 노출한다', () async {
|
||||
|
||||
@@ -106,7 +106,7 @@ void main() {
|
||||
() => transactionRepository.create(any()),
|
||||
).thenAnswer((_) async => original);
|
||||
when(
|
||||
() => transactionRepository.complete(any()),
|
||||
() => transactionRepository.complete(any(), note: any(named: 'note')),
|
||||
).thenAnswer((_) async => completed);
|
||||
|
||||
await controller.createTransaction(
|
||||
|
||||
@@ -164,7 +164,7 @@ void main() {
|
||||
status: StockTransactionStatus(id: 2, name: '완료'),
|
||||
);
|
||||
when(
|
||||
() => transactionRepository.complete(any()),
|
||||
() => transactionRepository.complete(any(), note: any(named: 'note')),
|
||||
).thenAnswer((_) async => updated);
|
||||
when(
|
||||
() => transactionRepository.list(filter: any(named: 'filter')),
|
||||
|
||||
@@ -215,16 +215,20 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await repository.submit(10);
|
||||
await repository.submit(10, note: '승인 요청');
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[0]
|
||||
as Map<String, dynamic>;
|
||||
expect(payload['id'], 10);
|
||||
expect(payload['note'], '승인 요청');
|
||||
});
|
||||
|
||||
test('complete는 /complete 엔드포인트를 호출한다', () async {
|
||||
@@ -244,16 +248,20 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await repository.complete(10);
|
||||
await repository.complete(10, note: '처리 완료');
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[0]
|
||||
as Map<String, dynamic>;
|
||||
expect(payload['id'], 10);
|
||||
expect(payload['note'], '처리 완료');
|
||||
});
|
||||
|
||||
test('approve는 /approve 엔드포인트를 호출한다', () async {
|
||||
@@ -273,16 +281,20 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await repository.approve(11);
|
||||
await repository.approve(11, note: '승인 확정');
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[0]
|
||||
as Map<String, dynamic>;
|
||||
expect(payload['id'], 11);
|
||||
expect(payload['note'], '승인 확정');
|
||||
});
|
||||
|
||||
test('reject는 /reject 엔드포인트를 호출한다', () async {
|
||||
@@ -302,16 +314,20 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await repository.reject(12);
|
||||
await repository.reject(12, note: '재작업 필요');
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[0]
|
||||
as Map<String, dynamic>;
|
||||
expect(payload['id'], 12);
|
||||
expect(payload['note'], '재작업 필요');
|
||||
});
|
||||
|
||||
test('cancel은 /cancel 엔드포인트를 호출한다', () async {
|
||||
@@ -331,15 +347,52 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await repository.cancel(13);
|
||||
await repository.cancel(13, note: '상신 취소');
|
||||
|
||||
verify(
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[0]
|
||||
as Map<String, dynamic>;
|
||||
expect(payload['id'], 13);
|
||||
expect(payload['note'], '상신 취소');
|
||||
});
|
||||
|
||||
test('transition note는 공백 입력 시 제외된다', () async {
|
||||
const path = '/api/v1/stock-transactions/99/submit';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.submit(99, note: ' ');
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[0]
|
||||
as Map<String, dynamic>;
|
||||
expect(payload['id'], 99);
|
||||
expect(payload.containsKey('note'), isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user