refactor: 인벤토리 테이블 스펙과 도메인 계층 정비
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/data/repositories/stock_transaction_repository_remote.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late StockTransactionRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = StockTransactionRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Future<Response<Map<String, dynamic>>> emptyListResponse(String path) async {
|
||||
return Response<Map<String, dynamic>>(
|
||||
data: {'items': const [], 'page': 1, 'page_size': 20, 'total': 0},
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> detailBody() {
|
||||
return {
|
||||
'data': {
|
||||
'id': 10,
|
||||
'transaction_no': 'TX-1',
|
||||
'transaction_type': {'id': 1, 'type_name': '입고'},
|
||||
'transaction_status': {'id': 1, 'status_name': '작성중'},
|
||||
'warehouse': {'id': 3, 'warehouse_code': 'W-1', 'warehouse_name': '서울'},
|
||||
'created_by': {'id': 5, 'employee_no': 'EMP-1', 'employee_name': '홍길동'},
|
||||
'lines': const [],
|
||||
'customers': const [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('list 호출 시 필터 파라미터를 전달한다', () async {
|
||||
const path = '/api/v1/stock-transactions';
|
||||
when(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
path,
|
||||
query: any(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer((_) => emptyListResponse(path));
|
||||
|
||||
final filter = StockTransactionListFilter(
|
||||
page: 2,
|
||||
pageSize: 50,
|
||||
query: '품번',
|
||||
transactionTypeId: 11,
|
||||
transactionStatusId: 7,
|
||||
warehouseId: 3,
|
||||
customerId: 99,
|
||||
from: DateTime(2024, 1, 1),
|
||||
to: DateTime(2024, 1, 31),
|
||||
sort: 'transaction_date',
|
||||
order: 'desc',
|
||||
include: const ['lines', 'approval'],
|
||||
);
|
||||
|
||||
await repository.list(filter: filter);
|
||||
|
||||
final captured = verify(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
query: captureAny(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured;
|
||||
|
||||
final query = captured[1] as Map<String, dynamic>;
|
||||
expect(captured.first, equals(path));
|
||||
expect(query['page'], 2);
|
||||
expect(query['page_size'], 50);
|
||||
expect(query['q'], '품번');
|
||||
expect(query['transaction_type_id'], 11);
|
||||
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['sort'], 'transaction_date');
|
||||
expect(query['order'], 'desc');
|
||||
expect(query['include'], 'lines,approval');
|
||||
});
|
||||
|
||||
test('fetchDetail은 include 파라미터를 조인해 전달한다', () async {
|
||||
const path = '/api/v1/stock-transactions/10';
|
||||
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: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.fetchDetail(10, include: const ['lines', 'customers']);
|
||||
|
||||
final query =
|
||||
verify(
|
||||
() => apiClient.get<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
query: captureAny(named: 'query'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(query['include'], 'lines,customers');
|
||||
});
|
||||
|
||||
test('create는 입력 payload를 body로 전달한다', () async {
|
||||
const path = '/api/v1/stock-transactions';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 201,
|
||||
),
|
||||
);
|
||||
|
||||
final input = StockTransactionCreateInput(
|
||||
transactionTypeId: 1,
|
||||
transactionStatusId: 2,
|
||||
warehouseId: 3,
|
||||
transactionDate: DateTime(2024, 2, 1),
|
||||
createdById: 9,
|
||||
note: '테스트',
|
||||
lines: [
|
||||
TransactionLineCreateInput(
|
||||
lineNo: 1,
|
||||
productId: 11,
|
||||
quantity: 2,
|
||||
unitPrice: 1000,
|
||||
),
|
||||
],
|
||||
customers: [TransactionCustomerCreateInput(customerId: 7)],
|
||||
);
|
||||
|
||||
await repository.create(input);
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(payload['transaction_type_id'], 1);
|
||||
expect(payload['transaction_status_id'], 2);
|
||||
expect(payload['warehouse_id'], 3);
|
||||
expect(payload['created_by_id'], 9);
|
||||
expect(payload['lines'], isA<List>());
|
||||
expect(payload['customers'], isA<List>());
|
||||
});
|
||||
|
||||
test('submit은 /submit 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/10/submit';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.submit(10);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('complete는 /complete 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/10/complete';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.complete(10);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('approve는 /approve 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/11/approve';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.approve(11);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('reject는 /reject 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/12/reject';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.reject(12);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('cancel은 /cancel 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/stock-transactions/13/cancel';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: detailBody(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.cancel(13);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/data/repositories/transaction_customer_repository_remote.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late TransactionCustomerRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = TransactionCustomerRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Map<String, dynamic> customerResponse() {
|
||||
return {
|
||||
'data': {
|
||||
'customers': [
|
||||
{
|
||||
'id': 301,
|
||||
'customer': {
|
||||
'id': 700,
|
||||
'customer_code': 'C-1',
|
||||
'customer_name': '슈퍼포트',
|
||||
},
|
||||
'note': '테스트',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('addCustomers는 거래 ID를 포함해 POST 요청을 보낸다', () async {
|
||||
const path = '/api/v1/stock-transactions/77/customers';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: customerResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.addCustomers(77, [
|
||||
TransactionCustomerCreateInput(customerId: 700, note: '비고'),
|
||||
]);
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(payload['id'], 77);
|
||||
expect(payload['customers'], isA<List>());
|
||||
});
|
||||
|
||||
test('updateCustomers는 PATCH 요청을 보낸다', () async {
|
||||
const path = '/api/v1/stock-transactions/77/customers';
|
||||
when(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: customerResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.updateCustomers(77, [
|
||||
TransactionCustomerUpdateInput(id: 301, note: '수정'),
|
||||
]);
|
||||
|
||||
verify(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('deleteCustomer는 /transaction-customers/{id} 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-customers/301';
|
||||
when(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<void>(
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 204,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.deleteCustomer(301);
|
||||
|
||||
verify(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/data/repositories/transaction_line_repository_remote.dart';
|
||||
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
void main() {
|
||||
late ApiClient apiClient;
|
||||
late TransactionLineRepositoryRemote repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = TransactionLineRepositoryRemote(apiClient: apiClient);
|
||||
});
|
||||
|
||||
Map<String, dynamic> lineResponse() {
|
||||
return {
|
||||
'data': {
|
||||
'lines': [
|
||||
{
|
||||
'id': 101,
|
||||
'line_no': 1,
|
||||
'product': {'id': 11, 'product_code': 'P-1', 'product_name': '품목'},
|
||||
'quantity': 3,
|
||||
'unit_price': 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('addLines는 거래 ID를 포함한 POST 요청을 보낸다', () async {
|
||||
const path = '/api/v1/stock-transactions/50/lines';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: lineResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.addLines(50, [
|
||||
TransactionLineCreateInput(
|
||||
lineNo: 2,
|
||||
productId: 44,
|
||||
quantity: 5,
|
||||
unitPrice: 1500,
|
||||
),
|
||||
]);
|
||||
|
||||
final payload =
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured[1]
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(payload['id'], 50);
|
||||
expect(payload['lines'], isA<List>());
|
||||
});
|
||||
|
||||
test('updateLines는 PATCH 요청을 사용한다', () async {
|
||||
const path = '/api/v1/stock-transactions/50/lines';
|
||||
when(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: lineResponse(),
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.updateLines(50, [
|
||||
TransactionLineUpdateInput(id: 101, quantity: 10),
|
||||
]);
|
||||
|
||||
verify(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('deleteLine은 /transaction-lines/{id}를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-lines/101';
|
||||
when(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<void>(
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 204,
|
||||
),
|
||||
);
|
||||
|
||||
await repository.deleteLine(101);
|
||||
|
||||
verify(
|
||||
() => apiClient.delete<void>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('restoreLine은 복구 엔드포인트를 호출한다', () async {
|
||||
const path = '/api/v1/transaction-lines/101/restore';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 101,
|
||||
'line_no': 1,
|
||||
'product': {'id': 11, 'product_code': 'P-1', 'product_name': '품목'},
|
||||
'quantity': 3,
|
||||
'unit_price': 1000,
|
||||
},
|
||||
},
|
||||
requestOptions: RequestOptions(path: path),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
|
||||
final line = await repository.restoreLine(101);
|
||||
|
||||
expect(line.id, 101);
|
||||
expect(line.lineNo, 1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user