feat(approvals): 결재 접근 차단 대응과 전표 전이 메모 전달 강화

- approvals 모듈에서 APPROVAL_ACCESS_DENIED 응답을 포착하여 ApprovalAccessDeniedException으로 변환하고 접근 거부 시 토스트·대시보드 리다이렉트를 처리

- approval history 조회가 서버 action id에 맞춰 필터링되도록 repository·controller·테스트를 보강

- 재고 트랜잭션 상태 전이 API 호출에 note를 전달하도록 repository·컨트롤러·통합/단위 테스트를 업데이트

- 승인 플로우 QA 체크리스트 및 연동 문서를 최신 계약과 테스트 흐름으로 업데이트
This commit is contained in:
JiWoong Sul
2025-10-31 16:43:14 +09:00
parent d76f765814
commit 3e83408aa7
35 changed files with 1056 additions and 470 deletions

View File

@@ -82,50 +82,43 @@ class StockTransactionRepositoryRemote implements StockTransactionRepository {
}
@override
Future<StockTransaction> submit(int id) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/$id/submit',
data: {'id': id},
options: Options(responseType: ResponseType.json),
);
return _parseSingle(response.data);
Future<StockTransaction> submit(int id, {String? note}) async {
return _postTransition(id: id, action: 'submit', note: note);
}
@override
Future<StockTransaction> complete(int id) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/$id/complete',
data: {'id': id},
options: Options(responseType: ResponseType.json),
);
return _parseSingle(response.data);
Future<StockTransaction> complete(int id, {String? note}) async {
return _postTransition(id: id, action: 'complete', note: note);
}
@override
Future<StockTransaction> approve(int id) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/$id/approve',
data: {'id': id},
options: Options(responseType: ResponseType.json),
);
return _parseSingle(response.data);
Future<StockTransaction> approve(int id, {String? note}) async {
return _postTransition(id: id, action: 'approve', note: note);
}
@override
Future<StockTransaction> reject(int id) async {
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/$id/reject',
data: {'id': id},
options: Options(responseType: ResponseType.json),
);
return _parseSingle(response.data);
Future<StockTransaction> reject(int id, {String? note}) async {
return _postTransition(id: id, action: 'reject', note: note);
}
@override
Future<StockTransaction> cancel(int id) async {
Future<StockTransaction> cancel(int id, {String? note}) async {
return _postTransition(id: id, action: 'cancel', note: note);
}
Future<StockTransaction> _postTransition({
required int id,
required String action,
String? note,
}) async {
final payload = <String, dynamic>{'id': id};
final trimmedNote = note?.trim();
if (trimmedNote != null && trimmedNote.isNotEmpty) {
payload['note'] = trimmedNote;
}
final response = await _api.post<Map<String, dynamic>>(
'$_basePath/$id/cancel',
data: {'id': id},
'$_basePath/$id/$action',
data: payload,
options: Options(responseType: ResponseType.json),
);
return _parseSingle(response.data);