feat(approval): 결재 최종 승인 시 전표 상태 자동 전이
- approval_controller에서 결재 단계 승인 완료 직후 전표 approve/complete API를 자동 호출하도록 연동\n- 승인/완료 상태명 판별 로직과 헬퍼를 보강해 출고·대여 전표도 동일하게 처리\n- approval_controller 테스트에 승인/완료/대기 자동 전이 시나리오를 추가해 회귀를 방지
This commit is contained in:
@@ -17,6 +17,8 @@ import '../../domain/repositories/approval_template_repository.dart';
|
|||||||
import '../../domain/usecases/get_approval_draft_use_case.dart';
|
import '../../domain/usecases/get_approval_draft_use_case.dart';
|
||||||
import '../../domain/usecases/list_approval_drafts_use_case.dart';
|
import '../../domain/usecases/list_approval_drafts_use_case.dart';
|
||||||
import '../../domain/usecases/save_approval_draft_use_case.dart';
|
import '../../domain/usecases/save_approval_draft_use_case.dart';
|
||||||
|
import '../../../inventory/transactions/domain/entities/stock_transaction.dart';
|
||||||
|
import '../../../inventory/transactions/domain/repositories/stock_transaction_repository.dart';
|
||||||
|
|
||||||
enum ApprovalStatusFilter {
|
enum ApprovalStatusFilter {
|
||||||
all,
|
all,
|
||||||
@@ -59,12 +61,14 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
ApprovalController({
|
ApprovalController({
|
||||||
required ApprovalRepository approvalRepository,
|
required ApprovalRepository approvalRepository,
|
||||||
required ApprovalTemplateRepository templateRepository,
|
required ApprovalTemplateRepository templateRepository,
|
||||||
|
required StockTransactionRepository transactionRepository,
|
||||||
InventoryLookupRepository? lookupRepository,
|
InventoryLookupRepository? lookupRepository,
|
||||||
SaveApprovalDraftUseCase? saveDraftUseCase,
|
SaveApprovalDraftUseCase? saveDraftUseCase,
|
||||||
GetApprovalDraftUseCase? getDraftUseCase,
|
GetApprovalDraftUseCase? getDraftUseCase,
|
||||||
ListApprovalDraftsUseCase? listDraftsUseCase,
|
ListApprovalDraftsUseCase? listDraftsUseCase,
|
||||||
}) : _repository = approvalRepository,
|
}) : _repository = approvalRepository,
|
||||||
_templateRepository = templateRepository,
|
_templateRepository = templateRepository,
|
||||||
|
_transactionRepository = transactionRepository,
|
||||||
_lookupRepository = lookupRepository,
|
_lookupRepository = lookupRepository,
|
||||||
_saveDraftUseCase = saveDraftUseCase,
|
_saveDraftUseCase = saveDraftUseCase,
|
||||||
_getDraftUseCase = getDraftUseCase,
|
_getDraftUseCase = getDraftUseCase,
|
||||||
@@ -72,6 +76,7 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
|
|
||||||
final ApprovalRepository _repository;
|
final ApprovalRepository _repository;
|
||||||
final ApprovalTemplateRepository _templateRepository;
|
final ApprovalTemplateRepository _templateRepository;
|
||||||
|
final StockTransactionRepository _transactionRepository;
|
||||||
final InventoryLookupRepository? _lookupRepository;
|
final InventoryLookupRepository? _lookupRepository;
|
||||||
final SaveApprovalDraftUseCase? _saveDraftUseCase;
|
final SaveApprovalDraftUseCase? _saveDraftUseCase;
|
||||||
final GetApprovalDraftUseCase? _getDraftUseCase;
|
final GetApprovalDraftUseCase? _getDraftUseCase;
|
||||||
@@ -79,8 +84,10 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
|
|
||||||
PaginatedResult<Approval>? _result;
|
PaginatedResult<Approval>? _result;
|
||||||
Approval? _selected;
|
Approval? _selected;
|
||||||
|
StockTransaction? _selectedTransaction;
|
||||||
bool _isLoadingList = false;
|
bool _isLoadingList = false;
|
||||||
bool _isLoadingDetail = false;
|
bool _isLoadingDetail = false;
|
||||||
|
bool _isLoadingTransactionDetail = false;
|
||||||
bool _isLoadingActions = false;
|
bool _isLoadingActions = false;
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
bool _isPerformingAction = false;
|
bool _isPerformingAction = false;
|
||||||
@@ -91,6 +98,7 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
ApprovalProceedStatus? _proceedStatus;
|
ApprovalProceedStatus? _proceedStatus;
|
||||||
ApprovalSubmissionInput? _submissionDraft;
|
ApprovalSubmissionInput? _submissionDraft;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
String? _transactionError;
|
||||||
bool _accessDenied = false;
|
bool _accessDenied = false;
|
||||||
String? _accessDeniedMessage;
|
String? _accessDeniedMessage;
|
||||||
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
ApprovalStatusFilter _statusFilter = ApprovalStatusFilter.all;
|
||||||
@@ -110,13 +118,16 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
|
|
||||||
PaginatedResult<Approval>? get result => _result;
|
PaginatedResult<Approval>? get result => _result;
|
||||||
Approval? get selected => _selected;
|
Approval? get selected => _selected;
|
||||||
|
StockTransaction? get selectedTransaction => _selectedTransaction;
|
||||||
bool get isLoadingList => _isLoadingList;
|
bool get isLoadingList => _isLoadingList;
|
||||||
bool get isLoadingDetail => _isLoadingDetail;
|
bool get isLoadingDetail => _isLoadingDetail;
|
||||||
|
bool get isLoadingTransactionDetail => _isLoadingTransactionDetail;
|
||||||
bool get isLoadingActions => _isLoadingActions;
|
bool get isLoadingActions => _isLoadingActions;
|
||||||
bool get isSubmitting => _isSubmitting;
|
bool get isSubmitting => _isSubmitting;
|
||||||
bool get isPerformingAction => _isPerformingAction;
|
bool get isPerformingAction => _isPerformingAction;
|
||||||
int? get processingStepId => _processingStepId;
|
int? get processingStepId => _processingStepId;
|
||||||
String? get errorMessage => _errorMessage;
|
String? get errorMessage => _errorMessage;
|
||||||
|
String? get transactionError => _transactionError;
|
||||||
bool get isAccessDenied => _accessDenied;
|
bool get isAccessDenied => _accessDenied;
|
||||||
String? get accessDeniedMessage => _accessDeniedMessage;
|
String? get accessDeniedMessage => _accessDeniedMessage;
|
||||||
ApprovalStatusFilter get statusFilter => _statusFilter;
|
ApprovalStatusFilter get statusFilter => _statusFilter;
|
||||||
@@ -398,6 +409,9 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
_isLoadingDetail = true;
|
_isLoadingDetail = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
_proceedStatus = null;
|
_proceedStatus = null;
|
||||||
|
_selectedTransaction = null;
|
||||||
|
_transactionError = null;
|
||||||
|
_isLoadingTransactionDetail = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final detail = await _repository.fetchDetail(
|
final detail = await _repository.fetchDetail(
|
||||||
@@ -409,12 +423,18 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
if (detail.id != null) {
|
if (detail.id != null) {
|
||||||
await _loadProceedStatus(detail.id!);
|
await _loadProceedStatus(detail.id!);
|
||||||
}
|
}
|
||||||
|
final transactionId = detail.transactionId;
|
||||||
|
if (transactionId != null) {
|
||||||
|
unawaited(_loadTransactionDetail(transactionId));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error is ApprovalAccessDeniedException) {
|
if (error is ApprovalAccessDeniedException) {
|
||||||
_accessDenied = true;
|
_accessDenied = true;
|
||||||
_accessDeniedMessage = error.message;
|
_accessDeniedMessage = error.message;
|
||||||
_selected = null;
|
_selected = null;
|
||||||
_proceedStatus = null;
|
_proceedStatus = null;
|
||||||
|
_selectedTransaction = null;
|
||||||
|
_transactionError = null;
|
||||||
} else {
|
} else {
|
||||||
final failure = Failure.from(error);
|
final failure = Failure.from(error);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -428,10 +448,40 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadTransactionDetail(int transactionId) async {
|
||||||
|
_isLoadingTransactionDetail = true;
|
||||||
|
_transactionError = null;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
final transaction = await _transactionRepository.fetchDetail(
|
||||||
|
transactionId,
|
||||||
|
include: const ['lines', 'customers', 'approval'],
|
||||||
|
);
|
||||||
|
if (_selected?.transactionId == transactionId) {
|
||||||
|
_selectedTransaction = transaction;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (_selected?.transactionId != transactionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final failure = Failure.from(error);
|
||||||
|
_transactionError = failure.describe();
|
||||||
|
_selectedTransaction = null;
|
||||||
|
} finally {
|
||||||
|
if (_selected?.transactionId == transactionId) {
|
||||||
|
_isLoadingTransactionDetail = false;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 선택된 결재 상세를 비우고 화면을 초기화한다.
|
/// 선택된 결재 상세를 비우고 화면을 초기화한다.
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
_selected = null;
|
_selected = null;
|
||||||
_proceedStatus = null;
|
_proceedStatus = null;
|
||||||
|
_selectedTransaction = null;
|
||||||
|
_transactionError = null;
|
||||||
|
_isLoadingTransactionDetail = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +512,122 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _syncTransactionStatusAfterFinalApproval({
|
||||||
|
required Approval approval,
|
||||||
|
required ApprovalStepActionType actionType,
|
||||||
|
String? note,
|
||||||
|
}) async {
|
||||||
|
if (actionType != ApprovalStepActionType.approve) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final transactionId = approval.transactionId;
|
||||||
|
if (transactionId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final syncAction = _resolveTransactionSyncAction(approval);
|
||||||
|
if (syncAction == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final sanitized = note?.trim();
|
||||||
|
final payloadNote = sanitized != null && sanitized.isNotEmpty
|
||||||
|
? sanitized
|
||||||
|
: '결재 최종 승인 자동 처리';
|
||||||
|
try {
|
||||||
|
switch (syncAction) {
|
||||||
|
case _TransactionSyncAction.approve:
|
||||||
|
await _transactionRepository.approve(
|
||||||
|
transactionId,
|
||||||
|
note: payloadNote,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case _TransactionSyncAction.complete:
|
||||||
|
await _transactionRepository.complete(
|
||||||
|
transactionId,
|
||||||
|
note: payloadNote,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unawaited(_loadTransactionDetail(transactionId));
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint(
|
||||||
|
'[ApprovalController] 전표 상태 연동 실패(tx=$transactionId, action=$syncAction): $error',
|
||||||
|
);
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_TransactionSyncAction? _resolveTransactionSyncAction(Approval approval) {
|
||||||
|
final normalizedName = approval.status.name.toLowerCase();
|
||||||
|
final statusCode = _statusCodeForId(approval.status.id);
|
||||||
|
if (_isApprovedStatus(statusCode, normalizedName)) {
|
||||||
|
return _TransactionSyncAction.approve;
|
||||||
|
}
|
||||||
|
if (_isCompletedStatus(statusCode, normalizedName)) {
|
||||||
|
return _TransactionSyncAction.complete;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _statusCodeForId(int id) {
|
||||||
|
final lookup = _statusLookup[id.toString()];
|
||||||
|
final code = lookup?.code?.trim();
|
||||||
|
if (code != null && code.isNotEmpty) {
|
||||||
|
return code.toLowerCase();
|
||||||
|
}
|
||||||
|
final name = lookup?.name.trim();
|
||||||
|
if (name != null && name.isNotEmpty) {
|
||||||
|
return name.toLowerCase();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isApprovedStatus(String? code, String normalizedName) {
|
||||||
|
if (code != null) {
|
||||||
|
final normalizedCode = code.toLowerCase();
|
||||||
|
if (normalizedCode == 'approved' || normalizedCode == 'approve') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_containsAny(normalizedName, const ['반려', '취소'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (normalizedName.contains('승인')) {
|
||||||
|
if (_containsAny(normalizedName, const ['대기', '요청', '진행'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (normalizedName.contains('approved')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isCompletedStatus(String? code, String normalizedName) {
|
||||||
|
if (code != null) {
|
||||||
|
final normalizedCode = code.toLowerCase();
|
||||||
|
if (normalizedCode == 'completed' || normalizedCode == 'complete') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalizedName.contains('완료') && !normalizedName.contains('승인')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (normalizedName.contains('completed')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _containsAny(String source, List<String> keywords) {
|
||||||
|
for (final keyword in keywords) {
|
||||||
|
if (source.contains(keyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<ApprovalSubmissionInput?> restoreSubmissionDraft({
|
Future<ApprovalSubmissionInput?> restoreSubmissionDraft({
|
||||||
required int requesterId,
|
required int requesterId,
|
||||||
int? transactionId,
|
int? transactionId,
|
||||||
@@ -651,8 +817,10 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final proceedStatus = await _repository.canProceed(approvalId);
|
final proceedStatus = await _repository.canProceed(approvalId);
|
||||||
_proceedStatus = proceedStatus;
|
_proceedStatus = proceedStatus;
|
||||||
if (!proceedStatus.canProceed) {
|
final actingOnCurrentStep = _isCurrentStep(step.id);
|
||||||
|
if (!proceedStatus.canProceed && !actingOnCurrentStep) {
|
||||||
_errorMessage = proceedStatus.reason ?? '결재 단계가 현재 상태에서 진행될 수 없습니다.';
|
_errorMessage = proceedStatus.reason ?? '결재 단계가 현재 상태에서 진행될 수 없습니다.';
|
||||||
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,6 +844,11 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
await _loadProceedStatus(approvalId);
|
await _loadProceedStatus(approvalId);
|
||||||
}
|
}
|
||||||
|
await _syncTransactionStatusAfterFinalApproval(
|
||||||
|
approval: updated,
|
||||||
|
actionType: type,
|
||||||
|
note: sanitizedNote,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
final failure = Failure.from(error);
|
final failure = Failure.from(error);
|
||||||
@@ -815,4 +988,24 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
_errorMessage ??= failure.describe();
|
_errorMessage ??= failure.describe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isCurrentStep(int? stepId) {
|
||||||
|
final current = _selected?.currentStep;
|
||||||
|
if (current == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stepId != null && current.id != null) {
|
||||||
|
return current.id == stepId;
|
||||||
|
}
|
||||||
|
if (stepId != null) {
|
||||||
|
for (final step in _selected?.steps ?? const <ApprovalStep>[]) {
|
||||||
|
if (step.id == stepId) {
|
||||||
|
return step.stepOrder == current.stepOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _TransactionSyncAction { approve, complete }
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import 'package:superport_v2/features/approvals/domain/usecases/save_approval_dr
|
|||||||
import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.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/entities/lookup_item.dart';
|
||||||
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
import 'package:superport_v2/features/inventory/lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction.dart';
|
||||||
|
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
|
||||||
|
|
||||||
import '../../../../helpers/fixture_loader.dart';
|
import '../../../../helpers/fixture_loader.dart';
|
||||||
|
|
||||||
@@ -36,6 +38,9 @@ class _FakeStepActionInput extends Fake implements ApprovalStepActionInput {}
|
|||||||
class _MockApprovalTemplateRepository extends Mock
|
class _MockApprovalTemplateRepository extends Mock
|
||||||
implements ApprovalTemplateRepository {}
|
implements ApprovalTemplateRepository {}
|
||||||
|
|
||||||
|
class _MockStockTransactionRepository extends Mock
|
||||||
|
implements StockTransactionRepository {}
|
||||||
|
|
||||||
/// 템플릿 단계 할당 요청을 대체하기 위한 가짜 입력.
|
/// 템플릿 단계 할당 요청을 대체하기 위한 가짜 입력.
|
||||||
class _FakeStepAssignmentInput extends Fake
|
class _FakeStepAssignmentInput extends Fake
|
||||||
implements ApprovalStepAssignmentInput {}
|
implements ApprovalStepAssignmentInput {}
|
||||||
@@ -56,12 +61,54 @@ void main() {
|
|||||||
late ApprovalController controller;
|
late ApprovalController controller;
|
||||||
late _MockApprovalRepository repository;
|
late _MockApprovalRepository repository;
|
||||||
late _MockApprovalTemplateRepository templateRepository;
|
late _MockApprovalTemplateRepository templateRepository;
|
||||||
|
late _MockStockTransactionRepository transactionRepository;
|
||||||
|
|
||||||
final sampleApproval = ApprovalDto.fromJson(
|
final sampleApproval = ApprovalDto.fromJson(
|
||||||
loadJsonFixture('approvals/approval_five_step_pending.json'),
|
loadJsonFixture('approvals/approval_five_step_pending.json'),
|
||||||
).toEntity();
|
).toEntity();
|
||||||
|
|
||||||
final sampleStep = sampleApproval.currentStep ?? sampleApproval.steps.first;
|
final sampleStep = sampleApproval.currentStep ?? sampleApproval.steps.first;
|
||||||
|
final sampleTransaction = StockTransaction(
|
||||||
|
id: 91001,
|
||||||
|
transactionNo: 'TRX-202511100001',
|
||||||
|
transactionDate: DateTime(2025, 9, 18),
|
||||||
|
type: StockTransactionType(id: 1, name: '입고'),
|
||||||
|
status: StockTransactionStatus(id: 1, name: '초안'),
|
||||||
|
warehouse: StockTransactionWarehouse(id: 1, code: 'WH-001', name: '1센터'),
|
||||||
|
createdBy: StockTransactionEmployee(
|
||||||
|
id: 7,
|
||||||
|
employeeNo: 'E2025001',
|
||||||
|
name: '김승인',
|
||||||
|
),
|
||||||
|
note: '테스트 전표',
|
||||||
|
lines: [
|
||||||
|
StockTransactionLine(
|
||||||
|
id: 12001,
|
||||||
|
lineNo: 1,
|
||||||
|
product: StockTransactionProduct(
|
||||||
|
id: 101,
|
||||||
|
code: 'P100',
|
||||||
|
name: '샘플',
|
||||||
|
vendor: StockTransactionVendorSummary(id: 10, name: '한빛상사'),
|
||||||
|
uom: StockTransactionUomSummary(id: 1, name: 'EA'),
|
||||||
|
),
|
||||||
|
quantity: 50,
|
||||||
|
unitPrice: 1200,
|
||||||
|
note: '비고',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
customers: [
|
||||||
|
StockTransactionCustomer(
|
||||||
|
id: 301,
|
||||||
|
customer: StockTransactionCustomerSummary(
|
||||||
|
id: 301,
|
||||||
|
code: 'C001',
|
||||||
|
name: 'ABC물류',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
expectedReturnDate: DateTime(2025, 9, 30),
|
||||||
|
);
|
||||||
|
|
||||||
/// 테스트용 페이징 응답을 생성하는 헬퍼.
|
/// 테스트용 페이징 응답을 생성하는 헬퍼.
|
||||||
PaginatedResult<Approval> createResult(List<Approval> items) {
|
PaginatedResult<Approval> createResult(List<Approval> items) {
|
||||||
@@ -90,9 +137,11 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
repository = _MockApprovalRepository();
|
repository = _MockApprovalRepository();
|
||||||
templateRepository = _MockApprovalTemplateRepository();
|
templateRepository = _MockApprovalTemplateRepository();
|
||||||
|
transactionRepository = _MockStockTransactionRepository();
|
||||||
controller = ApprovalController(
|
controller = ApprovalController(
|
||||||
approvalRepository: repository,
|
approvalRepository: repository,
|
||||||
templateRepository: templateRepository,
|
templateRepository: templateRepository,
|
||||||
|
transactionRepository: transactionRepository,
|
||||||
);
|
);
|
||||||
when(() => repository.canProceed(any())).thenAnswer(
|
when(() => repository.canProceed(any())).thenAnswer(
|
||||||
(_) async => ApprovalProceedStatus(
|
(_) async => ApprovalProceedStatus(
|
||||||
@@ -100,6 +149,12 @@ void main() {
|
|||||||
canProceed: true,
|
canProceed: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.fetchDetail(
|
||||||
|
any(),
|
||||||
|
include: any(named: 'include'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => sampleTransaction);
|
||||||
when(
|
when(
|
||||||
() => repository.list(
|
() => repository.list(
|
||||||
page: any(named: 'page'),
|
page: any(named: 'page'),
|
||||||
@@ -273,6 +328,7 @@ void main() {
|
|||||||
final ctrl = ApprovalController(
|
final ctrl = ApprovalController(
|
||||||
approvalRepository: repository,
|
approvalRepository: repository,
|
||||||
templateRepository: templateRepository,
|
templateRepository: templateRepository,
|
||||||
|
transactionRepository: transactionRepository,
|
||||||
lookupRepository: lookupRepository,
|
lookupRepository: lookupRepository,
|
||||||
);
|
);
|
||||||
when(
|
when(
|
||||||
@@ -340,6 +396,45 @@ void main() {
|
|||||||
expect(controller.canProceedSelected, isTrue);
|
expect(controller.canProceedSelected, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('트랜잭션 상세를 로드해 상태에 반영한다', () async {
|
||||||
|
when(
|
||||||
|
() => repository.fetchDetail(
|
||||||
|
any(),
|
||||||
|
includeSteps: any(named: 'includeSteps'),
|
||||||
|
includeHistories: any(named: 'includeHistories'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => sampleApproval);
|
||||||
|
|
||||||
|
await controller.selectApproval(sampleApproval.id!);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(controller.selectedTransaction, isNotNull);
|
||||||
|
expect(controller.selectedTransaction!.id, sampleTransaction.id);
|
||||||
|
expect(controller.transactionError, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('트랜잭션 조회 실패 시 오류 상태를 기록한다', () async {
|
||||||
|
when(
|
||||||
|
() => repository.fetchDetail(
|
||||||
|
any(),
|
||||||
|
includeSteps: any(named: 'includeSteps'),
|
||||||
|
includeHistories: any(named: 'includeHistories'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => sampleApproval);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.fetchDetail(
|
||||||
|
any(),
|
||||||
|
include: any(named: 'include'),
|
||||||
|
),
|
||||||
|
).thenThrow(Exception('tx fail'));
|
||||||
|
|
||||||
|
await controller.selectApproval(sampleApproval.id!);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(controller.selectedTransaction, isNull);
|
||||||
|
expect(controller.transactionError, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
test('상세 조회에서 접근 거부 시 accessDenied를 기록한다', () async {
|
test('상세 조회에서 접근 거부 시 accessDenied를 기록한다', () async {
|
||||||
when(
|
when(
|
||||||
() => repository.fetchDetail(
|
() => repository.fetchDetail(
|
||||||
@@ -543,6 +638,114 @@ void main() {
|
|||||||
verify(() => repository.performStepAction(any())).called(1);
|
verify(() => repository.performStepAction(any())).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('최종 승인 시 연결 전표를 승인 처리한다', () async {
|
||||||
|
final approvedStatus = ApprovalStatus(
|
||||||
|
id: 7,
|
||||||
|
name: '승인',
|
||||||
|
isTerminal: true,
|
||||||
|
);
|
||||||
|
final approvedStep = updatedStep.copyWith(status: approvedStatus);
|
||||||
|
final finalApproval = sampleApproval.copyWith(
|
||||||
|
status: approvedStatus,
|
||||||
|
currentStep: approvedStep,
|
||||||
|
steps: [approvedStep],
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => repository.performStepAction(any()),
|
||||||
|
).thenAnswer((_) async => finalApproval);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.approve(any(), note: any(named: 'note')),
|
||||||
|
).thenAnswer((_) async => sampleTransaction);
|
||||||
|
|
||||||
|
await controller.loadActionOptions(force: true);
|
||||||
|
await controller.fetch();
|
||||||
|
await controller.selectApproval(sampleApproval.id!);
|
||||||
|
|
||||||
|
final success = await controller.performStepAction(
|
||||||
|
step: sampleStep,
|
||||||
|
type: ApprovalStepActionType.approve,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(success, isTrue);
|
||||||
|
verify(
|
||||||
|
() => transactionRepository.approve(
|
||||||
|
sampleApproval.transactionId!,
|
||||||
|
note: any(named: 'note'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('완료 상태면 연결 전표를 완료 처리한다', () async {
|
||||||
|
final completedStatus = ApprovalStatus(
|
||||||
|
id: 8,
|
||||||
|
name: '처리완료',
|
||||||
|
isTerminal: true,
|
||||||
|
);
|
||||||
|
final completedStep = updatedStep.copyWith(status: completedStatus);
|
||||||
|
final finalApproval = sampleApproval.copyWith(
|
||||||
|
status: completedStatus,
|
||||||
|
currentStep: completedStep,
|
||||||
|
steps: [completedStep],
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => repository.performStepAction(any()),
|
||||||
|
).thenAnswer((_) async => finalApproval);
|
||||||
|
when(
|
||||||
|
() => transactionRepository.complete(any(), note: any(named: 'note')),
|
||||||
|
).thenAnswer((_) async => sampleTransaction);
|
||||||
|
|
||||||
|
await controller.loadActionOptions(force: true);
|
||||||
|
await controller.fetch();
|
||||||
|
await controller.selectApproval(sampleApproval.id!);
|
||||||
|
|
||||||
|
final success = await controller.performStepAction(
|
||||||
|
step: sampleStep,
|
||||||
|
type: ApprovalStepActionType.approve,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(success, isTrue);
|
||||||
|
verify(
|
||||||
|
() => transactionRepository.complete(
|
||||||
|
sampleApproval.transactionId!,
|
||||||
|
note: any(named: 'note'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('승인 대기 상태면 전표 전이를 실행하지 않는다', () async {
|
||||||
|
final pendingStatus = ApprovalStatus(
|
||||||
|
id: 9,
|
||||||
|
name: '승인대기',
|
||||||
|
isTerminal: false,
|
||||||
|
);
|
||||||
|
final pendingStep = updatedStep.copyWith(status: pendingStatus);
|
||||||
|
final pendingApproval = sampleApproval.copyWith(
|
||||||
|
status: pendingStatus,
|
||||||
|
currentStep: pendingStep,
|
||||||
|
steps: [pendingStep],
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => repository.performStepAction(any()),
|
||||||
|
).thenAnswer((_) async => pendingApproval);
|
||||||
|
|
||||||
|
await controller.loadActionOptions(force: true);
|
||||||
|
await controller.fetch();
|
||||||
|
await controller.selectApproval(sampleApproval.id!);
|
||||||
|
|
||||||
|
final success = await controller.performStepAction(
|
||||||
|
step: sampleStep,
|
||||||
|
type: ApprovalStepActionType.approve,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(success, isTrue);
|
||||||
|
verifyNever(
|
||||||
|
() => transactionRepository.approve(any(), note: any(named: 'note')),
|
||||||
|
);
|
||||||
|
verifyNever(
|
||||||
|
() => transactionRepository.complete(any(), note: any(named: 'note')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('예외 발생 시 errorMessage 설정', () async {
|
test('예외 발생 시 errorMessage 설정', () async {
|
||||||
when(
|
when(
|
||||||
() => repository.performStepAction(any()),
|
() => repository.performStepAction(any()),
|
||||||
@@ -795,6 +998,7 @@ void main() {
|
|||||||
final controllerWithSave = ApprovalController(
|
final controllerWithSave = ApprovalController(
|
||||||
approvalRepository: repository,
|
approvalRepository: repository,
|
||||||
templateRepository: templateRepository,
|
templateRepository: templateRepository,
|
||||||
|
transactionRepository: transactionRepository,
|
||||||
saveDraftUseCase: saveUseCase,
|
saveDraftUseCase: saveUseCase,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -855,6 +1059,7 @@ void main() {
|
|||||||
final controllerWithDrafts = ApprovalController(
|
final controllerWithDrafts = ApprovalController(
|
||||||
approvalRepository: repository,
|
approvalRepository: repository,
|
||||||
templateRepository: templateRepository,
|
templateRepository: templateRepository,
|
||||||
|
transactionRepository: transactionRepository,
|
||||||
listDraftsUseCase: listUseCase,
|
listDraftsUseCase: listUseCase,
|
||||||
getDraftUseCase: getUseCase,
|
getDraftUseCase: getUseCase,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user