feat(approvals): 결재 상세 전표 연동과 스코프 권한 매핑 확장
- 결재 상세 다이얼로그에 전표 요약·라인·고객 섹션을 추가하고 현재 사용자 단계 강조 및 비고 입력 검증을 개선함 - 대시보드·결재 목록에서 전표 리포지토리와 AuthService를 주입해 상세 진입과 결재 관리 이동 버튼을 제공함 - StockTransactionApprovalInput이 template/steps를 config 노드로 직렬화하도록 변경하고 통합 테스트를 갱신함 - scope 권한 문자열을 리소스권으로 변환하는 PermissionScopeMapper와 단위 테스트를 추가하고 AuthPermission을 연동함 - 재고 메뉴 정렬, 상세 컨트롤러 오류 리셋, 요청자 자동완성 상태 동기화 등 주변 UI 버그를 수정하고 테스트를 보강함
This commit is contained in:
48
test/core/permissions/permission_scope_mapper_test.dart
Normal file
48
test/core/permissions/permission_scope_mapper_test.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||
import 'package:superport_v2/core/permissions/permission_scope_mapper.dart';
|
||||
|
||||
void main() {
|
||||
group('PermissionScopeMapper', () {
|
||||
test('maps approval scopes to approval resource', () {
|
||||
final map = PermissionScopeMapper.map('approval.approve');
|
||||
|
||||
expect(map, isNotNull);
|
||||
expect(
|
||||
map![PermissionResources.approvals],
|
||||
contains(PermissionAction.approve),
|
||||
);
|
||||
});
|
||||
|
||||
test('maps approvals.* codes to view permissions', () {
|
||||
final map = PermissionScopeMapper.map('approvals.templates');
|
||||
|
||||
expect(map, isNotNull);
|
||||
expect(
|
||||
map![PermissionResources.approvalTemplates],
|
||||
contains(PermissionAction.view),
|
||||
);
|
||||
});
|
||||
|
||||
test('maps dashboard codes to dashboard resource', () {
|
||||
final map = PermissionScopeMapper.map('dashboard');
|
||||
|
||||
expect(map, isNotNull);
|
||||
expect(
|
||||
map![PermissionResources.dashboard],
|
||||
contains(PermissionAction.view),
|
||||
);
|
||||
});
|
||||
|
||||
test('maps inventory transaction codes to stock transactions resource', () {
|
||||
final map = PermissionScopeMapper.map('inventory.issues');
|
||||
|
||||
expect(map, isNotNull);
|
||||
expect(
|
||||
map![PermissionResources.stockTransactions],
|
||||
contains(PermissionAction.view),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import 'package:superport_v2/features/approvals/domain/repositories/approval_rep
|
||||
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/approvals/presentation/dialogs/approval_detail_dialog.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';
|
||||
import 'package:superport_v2/widgets/components/superport_dialog.dart';
|
||||
|
||||
import '../../../../helpers/test_app.dart';
|
||||
@@ -23,17 +26,63 @@ void main() {
|
||||
|
||||
late _FakeApprovalRepository approvalRepository;
|
||||
late _FakeApprovalTemplateRepository templateRepository;
|
||||
late _FakeStockTransactionRepository stockRepository;
|
||||
late ApprovalController controller;
|
||||
late Approval sampleApproval;
|
||||
late ApprovalTemplate sampleTemplate;
|
||||
late StockTransaction sampleTransaction;
|
||||
|
||||
setUp(() async {
|
||||
approvalRepository = _FakeApprovalRepository();
|
||||
templateRepository = _FakeApprovalTemplateRepository();
|
||||
stockRepository = _FakeStockTransactionRepository();
|
||||
sampleTransaction = StockTransaction(
|
||||
id: 91001,
|
||||
transactionNo: 'TRX-100',
|
||||
transactionDate: DateTime(2024, 1, 1, 9),
|
||||
type: StockTransactionType(id: 1, name: '입고'),
|
||||
status: StockTransactionStatus(id: 1, name: '초안'),
|
||||
warehouse: StockTransactionWarehouse(id: 10, code: 'WH-001', name: '1센터'),
|
||||
createdBy: StockTransactionEmployee(
|
||||
id: 99,
|
||||
employeeNo: 'E099',
|
||||
name: '요청자',
|
||||
),
|
||||
note: '전표 비고',
|
||||
lines: [
|
||||
StockTransactionLine(
|
||||
id: 1001,
|
||||
lineNo: 1,
|
||||
product: StockTransactionProduct(
|
||||
id: 501,
|
||||
code: 'P-501',
|
||||
name: '샘플 제품',
|
||||
vendor: StockTransactionVendorSummary(id: 7, name: '한빛상사'),
|
||||
uom: StockTransactionUomSummary(id: 3, name: 'EA'),
|
||||
),
|
||||
quantity: 12,
|
||||
unitPrice: 0,
|
||||
note: '라인 비고',
|
||||
),
|
||||
],
|
||||
customers: [
|
||||
StockTransactionCustomer(
|
||||
id: 9001,
|
||||
customer: StockTransactionCustomerSummary(
|
||||
id: 4001,
|
||||
code: 'C-4001',
|
||||
name: '고객A',
|
||||
),
|
||||
),
|
||||
],
|
||||
expectedReturnDate: DateTime(2024, 1, 10),
|
||||
);
|
||||
stockRepository.detail = sampleTransaction;
|
||||
|
||||
controller = ApprovalController(
|
||||
approvalRepository: approvalRepository,
|
||||
templateRepository: templateRepository,
|
||||
transactionRepository: stockRepository,
|
||||
);
|
||||
|
||||
final statusInProgress = ApprovalStatus(id: 1, name: '진행중');
|
||||
@@ -55,8 +104,10 @@ void main() {
|
||||
sampleApproval = Approval(
|
||||
id: 100,
|
||||
approvalNo: 'APP-2024-0100',
|
||||
transactionNo: 'TRX-100',
|
||||
transactionId: sampleTransaction.id,
|
||||
transactionNo: sampleTransaction.transactionNo,
|
||||
status: statusInProgress,
|
||||
currentStep: step,
|
||||
requester: requester,
|
||||
requestedAt: DateTime(2024, 1, 1, 9),
|
||||
steps: [step],
|
||||
@@ -100,6 +151,7 @@ void main() {
|
||||
await controller.loadTemplates(force: true);
|
||||
await controller.loadActionOptions(force: true);
|
||||
await controller.selectApproval(sampleApproval.id!);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
expect(controller.templates, isNotEmpty);
|
||||
expect(controller.selected, isNotNull);
|
||||
expect(controller.canProceedSelected, isTrue);
|
||||
@@ -123,6 +175,7 @@ void main() {
|
||||
dateFormat: dateFormat,
|
||||
canPerformStepActions: true,
|
||||
canApplyTemplate: true,
|
||||
currentUserId: sampleApproval.steps.first.approver.id,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -386,3 +439,71 @@ class _FakeApprovalTemplateRepository implements ApprovalTemplateRepository {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeStockTransactionRepository implements StockTransactionRepository {
|
||||
StockTransaction? detail;
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<StockTransaction>> list({
|
||||
StockTransactionListFilter? filter,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> fetchDetail(
|
||||
int id, {
|
||||
List<String> include = const ['lines', 'customers', 'approval'],
|
||||
}) async {
|
||||
final result = detail;
|
||||
if (result == null) {
|
||||
throw StateError('transaction detail not set');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> create(StockTransactionCreateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> update(int id, StockTransactionUpdateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> restore(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> submit(int id, {String? note}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> complete(int id, {String? note}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> approve(int id, {String? note}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> reject(int id, {String? note}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StockTransaction> cancel(int id, {String? note}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||
import 'package:superport_v2/core/permissions/permission_resources.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/auth_permission.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthPermission', () {
|
||||
test('scope 리소스는 actions가 비어도 view 권한을 부여한다', () {
|
||||
group('AuthPermission.toPermissionMap', () {
|
||||
test('approval.approve scope grants approval resource approve action', () {
|
||||
const permission = AuthPermission(
|
||||
resource: 'scope:inventory.view',
|
||||
resource: 'scope:approval.approve',
|
||||
actions: [],
|
||||
);
|
||||
|
||||
final map = permission.toPermissionMap();
|
||||
|
||||
expect(map.length, 1);
|
||||
expect(
|
||||
map[PermissionResources.inventoryScope],
|
||||
contains(PermissionAction.view),
|
||||
map[PermissionResources.approvals],
|
||||
contains(PermissionAction.approve),
|
||||
);
|
||||
expect(map['scope:approval.approve'], contains(PermissionAction.view));
|
||||
});
|
||||
|
||||
test('approval.manage scope grants manage actions to approval modules', () {
|
||||
const permission = AuthPermission(
|
||||
resource: 'scope:approval.manage',
|
||||
actions: [],
|
||||
);
|
||||
|
||||
final map = permission.toPermissionMap();
|
||||
|
||||
expect(
|
||||
map[PermissionResources.approvals],
|
||||
containsAll({
|
||||
PermissionAction.view,
|
||||
PermissionAction.create,
|
||||
PermissionAction.edit,
|
||||
PermissionAction.delete,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
map[PermissionResources.approvalTemplates],
|
||||
contains(PermissionAction.edit),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user