feat(android): add attachment API methods and result classes
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -299,6 +299,169 @@ class BoxDeleteResult {
|
|||||||
BoxDeleteResult({required this.success, this.errorMessage});
|
BoxDeleteResult({required this.success, this.errorMessage});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 附件模块数据类 ===
|
||||||
|
|
||||||
|
class AttachmentCategoryData {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final int sortOrder;
|
||||||
|
|
||||||
|
AttachmentCategoryData({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.sortOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AttachmentCategoryData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AttachmentCategoryData(
|
||||||
|
id: json['id'] as int,
|
||||||
|
name: json['name'] as String,
|
||||||
|
sortOrder: json['sort_order'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentCategoryResult {
|
||||||
|
final bool success;
|
||||||
|
final String? errorMessage;
|
||||||
|
final List<AttachmentCategoryData> categories;
|
||||||
|
|
||||||
|
AttachmentCategoryResult({
|
||||||
|
required this.success,
|
||||||
|
this.errorMessage,
|
||||||
|
this.categories = const [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentStatusItemData {
|
||||||
|
final int categoryId;
|
||||||
|
final String name;
|
||||||
|
final int expectedQty;
|
||||||
|
final int boxedQty;
|
||||||
|
final String? locationCode;
|
||||||
|
final bool complete;
|
||||||
|
|
||||||
|
AttachmentStatusItemData({
|
||||||
|
required this.categoryId,
|
||||||
|
required this.name,
|
||||||
|
required this.expectedQty,
|
||||||
|
required this.boxedQty,
|
||||||
|
this.locationCode,
|
||||||
|
required this.complete,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AttachmentStatusItemData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AttachmentStatusItemData(
|
||||||
|
categoryId: json['category_id'] as int,
|
||||||
|
name: json['name'] as String,
|
||||||
|
expectedQty: json['expected_qty'] as int,
|
||||||
|
boxedQty: json['boxed_qty'] as int,
|
||||||
|
locationCode: json['location_code']?.toString(),
|
||||||
|
complete: json['complete'] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentStatusResult {
|
||||||
|
final bool success;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String determination;
|
||||||
|
final bool allComplete;
|
||||||
|
final List<AttachmentStatusItemData> items;
|
||||||
|
|
||||||
|
AttachmentStatusResult({
|
||||||
|
required this.success,
|
||||||
|
this.errorMessage,
|
||||||
|
this.determination = 'undetermined',
|
||||||
|
this.allComplete = false,
|
||||||
|
this.items = const [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentConfigItemData {
|
||||||
|
final int categoryId;
|
||||||
|
final int expectedQty;
|
||||||
|
|
||||||
|
const AttachmentConfigItemData({
|
||||||
|
required this.categoryId,
|
||||||
|
required this.expectedQty,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'category_id': categoryId,
|
||||||
|
'expected_qty': expectedQty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentConfigResult {
|
||||||
|
final bool success;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String? errorCode;
|
||||||
|
final String determination;
|
||||||
|
final bool allComplete;
|
||||||
|
final List<AttachmentStatusItemData> items;
|
||||||
|
|
||||||
|
AttachmentConfigResult({
|
||||||
|
required this.success,
|
||||||
|
this.errorMessage,
|
||||||
|
this.errorCode,
|
||||||
|
this.determination = 'undetermined',
|
||||||
|
this.allComplete = false,
|
||||||
|
this.items = const [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentLocationResult {
|
||||||
|
final bool success;
|
||||||
|
final bool isDuplicate;
|
||||||
|
final bool isAlreadyOffShelf;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String? locationCode;
|
||||||
|
final String? previousLocation;
|
||||||
|
final Map<String, dynamic>? conflictInfo;
|
||||||
|
|
||||||
|
AttachmentLocationResult({
|
||||||
|
required this.success,
|
||||||
|
this.isDuplicate = false,
|
||||||
|
this.isAlreadyOffShelf = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.locationCode,
|
||||||
|
this.previousLocation,
|
||||||
|
this.conflictInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentBoxResult {
|
||||||
|
final bool success;
|
||||||
|
final bool isDuplicate;
|
||||||
|
final String? errorMessage;
|
||||||
|
final int? boxItemId;
|
||||||
|
final String? paichanNo;
|
||||||
|
final int? boxNo;
|
||||||
|
final String? zongpaiNo;
|
||||||
|
final int? categoryId;
|
||||||
|
final int? quantity;
|
||||||
|
|
||||||
|
AttachmentBoxResult({
|
||||||
|
required this.success,
|
||||||
|
this.isDuplicate = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.boxItemId,
|
||||||
|
this.paichanNo,
|
||||||
|
this.boxNo,
|
||||||
|
this.zongpaiNo,
|
||||||
|
this.categoryId,
|
||||||
|
this.quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentBoxDeleteResult {
|
||||||
|
final bool success;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
AttachmentBoxDeleteResult({required this.success, this.errorMessage});
|
||||||
|
}
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
final http.Client _client;
|
final http.Client _client;
|
||||||
final Duration timeout;
|
final Duration timeout;
|
||||||
@@ -534,4 +697,341 @@ class ApiService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 查询附件类型列表 — GET /CargoTrace/attachment/categories
|
||||||
|
Future<AttachmentCategoryResult> fetchAttachmentCategories({
|
||||||
|
required String baseUrl,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/CargoTrace/attachment/categories');
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.get(uri, headers: {'Content-Type': 'application/json'})
|
||||||
|
.timeout(timeout);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final list = (jsonDecode(response.body) as List)
|
||||||
|
.map(
|
||||||
|
(e) => AttachmentCategoryData.fromJson(e as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return AttachmentCategoryResult(success: true, categories: list);
|
||||||
|
}
|
||||||
|
return AttachmentCategoryResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '加载失败 (${response.statusCode})',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentCategoryResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '网络异常,请检查网络连接',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询附件状态 — GET /CargoTrace/attachment/status
|
||||||
|
Future<AttachmentStatusResult> fetchAttachmentStatus({
|
||||||
|
required String baseUrl,
|
||||||
|
required String zongpaiNo,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'$baseUrl/CargoTrace/attachment/status',
|
||||||
|
).replace(queryParameters: {'zongpai_no': zongpaiNo});
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.get(uri, headers: {'Content-Type': 'application/json'})
|
||||||
|
.timeout(timeout);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final items =
|
||||||
|
(body['items'] as List?)
|
||||||
|
?.map(
|
||||||
|
(e) => AttachmentStatusItemData.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
return AttachmentStatusResult(
|
||||||
|
success: true,
|
||||||
|
determination: body['determination']?.toString() ?? 'undetermined',
|
||||||
|
allComplete: body['all_complete'] as bool? ?? false,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AttachmentStatusResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '查询失败 (${response.statusCode})',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentStatusResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '网络异常,请检查网络连接',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 定稿附件清单 — PUT /CargoTrace/attachment/config
|
||||||
|
Future<AttachmentConfigResult> putAttachmentConfig({
|
||||||
|
required String baseUrl,
|
||||||
|
required String zongpaiNo,
|
||||||
|
required String determination,
|
||||||
|
required List<AttachmentConfigItemData> items,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/CargoTrace/attachment/config');
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.put(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'zongpai_no': zongpaiNo,
|
||||||
|
'determination': determination,
|
||||||
|
'items': items.map((i) => i.toJson()).toList(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final respItems =
|
||||||
|
(body['items'] as List?)
|
||||||
|
?.map(
|
||||||
|
(e) => AttachmentStatusItemData.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
return AttachmentConfigResult(
|
||||||
|
success: true,
|
||||||
|
determination: body['determination']?.toString() ?? 'has',
|
||||||
|
allComplete: body['all_complete'] as bool? ?? false,
|
||||||
|
items: respItems,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentConfigResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: body['message']?.toString() ?? '配置失败',
|
||||||
|
errorCode: body['error_code']?.toString(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentConfigResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '网络异常,请检查网络连接',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 附件上架 — POST /CargoTrace/attachment/location
|
||||||
|
Future<AttachmentLocationResult> registerAttachmentLocation({
|
||||||
|
required String baseUrl,
|
||||||
|
required String zongpaiNo,
|
||||||
|
required int categoryId,
|
||||||
|
required String locationCode,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/CargoTrace/attachment/location');
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.post(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'zongpai_no': zongpaiNo,
|
||||||
|
'category_id': categoryId,
|
||||||
|
'location_code': locationCode,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: true,
|
||||||
|
locationCode: body['location_code']?.toString(),
|
||||||
|
previousLocation: body['previous_location']?.toString(),
|
||||||
|
);
|
||||||
|
case 400:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: body['message']?.toString() ?? '请求参数错误',
|
||||||
|
);
|
||||||
|
case 409:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final errorCode = body['error_code']?.toString();
|
||||||
|
if (errorCode == 'DUPLICATE_LOCATION') {
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: false,
|
||||||
|
isDuplicate: true,
|
||||||
|
conflictInfo: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (errorCode == 'ALREADY_OFF_SHELF') {
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: false,
|
||||||
|
isAlreadyOffShelf: true,
|
||||||
|
errorMessage: body['message']?.toString(),
|
||||||
|
conflictInfo: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: body['message']?.toString() ?? '提交失败',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '提交失败 (${response.statusCode})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentLocationResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '网络异常,请检查网络连接',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 附件装箱 — POST /CargoTrace/attachment/box
|
||||||
|
Future<AttachmentBoxResult> saveAttachmentBox({
|
||||||
|
required String baseUrl,
|
||||||
|
required String zongpaiNo,
|
||||||
|
required int categoryId,
|
||||||
|
required int boxNo,
|
||||||
|
required int quantity,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/CargoTrace/attachment/box');
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.post(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'zongpai_no': zongpaiNo,
|
||||||
|
'category_id': categoryId,
|
||||||
|
'box_no': boxNo,
|
||||||
|
'quantity': quantity,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: true,
|
||||||
|
boxItemId: body['box_item_id'] as int?,
|
||||||
|
paichanNo: body['paichan_no']?.toString(),
|
||||||
|
boxNo: body['box_no'] as int?,
|
||||||
|
zongpaiNo: body['zongpai_no']?.toString(),
|
||||||
|
categoryId: body['category_id'] as int?,
|
||||||
|
quantity: body['quantity'] as int?,
|
||||||
|
);
|
||||||
|
case 400:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: body['message']?.toString() ?? '请求参数错误',
|
||||||
|
);
|
||||||
|
case 404:
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '未找到该总排号或附件类型',
|
||||||
|
);
|
||||||
|
case 409:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: false,
|
||||||
|
isDuplicate: true,
|
||||||
|
errorMessage: body['message']?.toString(),
|
||||||
|
paichanNo: body['paichan_no']?.toString(),
|
||||||
|
boxNo: body['box_no'] as int?,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '提交失败 (${response.statusCode})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentBoxResult(success: false, errorMessage: '网络异常,请检查网络连接');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改附件装箱明细 — PATCH /CargoTrace/attachment/box/{itemId}
|
||||||
|
Future<AttachmentBoxResult> updateAttachmentBox({
|
||||||
|
required String baseUrl,
|
||||||
|
required int boxItemId,
|
||||||
|
required int boxNo,
|
||||||
|
required int quantity,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/CargoTrace/attachment/box/$boxItemId');
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.patch(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({'box_no': boxNo, 'quantity': quantity}),
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: true,
|
||||||
|
boxItemId: body['box_item_id'] as int?,
|
||||||
|
paichanNo: body['paichan_no']?.toString(),
|
||||||
|
boxNo: body['box_no'] as int?,
|
||||||
|
zongpaiNo: body['zongpai_no']?.toString(),
|
||||||
|
categoryId: body['category_id'] as int?,
|
||||||
|
quantity: body['quantity'] as int?,
|
||||||
|
);
|
||||||
|
case 400:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: body['message']?.toString() ?? '请求参数错误',
|
||||||
|
);
|
||||||
|
case 404:
|
||||||
|
return AttachmentBoxResult(success: false, errorMessage: '指定装箱明细不存在');
|
||||||
|
default:
|
||||||
|
return AttachmentBoxResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '修改失败 (${response.statusCode})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentBoxResult(success: false, errorMessage: '网络异常,请检查网络连接');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除附件装箱明细 — DELETE /CargoTrace/attachment/box/{itemId}
|
||||||
|
Future<AttachmentBoxDeleteResult> deleteAttachmentBox({
|
||||||
|
required String baseUrl,
|
||||||
|
required int boxItemId,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/CargoTrace/attachment/box/$boxItemId');
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.delete(uri, headers: {'Content-Type': 'application/json'})
|
||||||
|
.timeout(timeout);
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
return AttachmentBoxDeleteResult(success: true);
|
||||||
|
case 404:
|
||||||
|
return AttachmentBoxDeleteResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '指定装箱明细不存在',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return AttachmentBoxDeleteResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '删除失败 (${response.statusCode})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return AttachmentBoxDeleteResult(
|
||||||
|
success: false,
|
||||||
|
errorMessage: '网络异常,请检查网络连接',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,6 +372,248 @@ void main() {
|
|||||||
expect(missing.errorMessage, '指定装箱明细不存在');
|
expect(missing.errorMessage, '指定装箱明细不存在');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('ApiService attachment APIs', () {
|
||||||
|
test('fetchAttachmentCategories parses list', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode([
|
||||||
|
{'id': 1, 'name': '检验证书', 'sort_order': 1},
|
||||||
|
{'id': 2, 'name': '增发', 'sort_order': 2},
|
||||||
|
]),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchAttachmentCategories(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.categories.length, 2);
|
||||||
|
expect(result.categories.first.name, '检验证书');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchAttachmentCategories handles network failure', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
throw Exception('Connection refused');
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchAttachmentCategories(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
);
|
||||||
|
expect(result.success, isFalse);
|
||||||
|
expect(result.errorMessage, '网络异常,请检查网络连接');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchAttachmentStatus parses undetermined zongpai', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'determination': 'undetermined',
|
||||||
|
'all_complete': false,
|
||||||
|
'items': [],
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchAttachmentStatus(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.determination, 'undetermined');
|
||||||
|
expect(result.items, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('putAttachmentConfig sends full payload', () async {
|
||||||
|
late Map<String, dynamic> body;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
final req = request as http.Request;
|
||||||
|
body = jsonDecode(req.body) as Map<String, dynamic>;
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'determination': 'has',
|
||||||
|
'all_complete': false,
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'category_id': 1,
|
||||||
|
'name': '检验证书',
|
||||||
|
'expected_qty': 80,
|
||||||
|
'boxed_qty': 0,
|
||||||
|
'location_code': null,
|
||||||
|
'complete': false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.putAttachmentConfig(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
determination: 'has',
|
||||||
|
items: [AttachmentConfigItemData(categoryId: 1, expectedQty: 80)],
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.items.single.complete, isFalse);
|
||||||
|
expect(body['items'], [
|
||||||
|
{'category_id': 1, 'expected_qty': 80},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registerAttachmentLocation returns 200 with data', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'category_id': 1,
|
||||||
|
'location_code': 'A01-01-01',
|
||||||
|
'created_at': '2026-08-12T10:00:00',
|
||||||
|
'previous_location': null,
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.registerAttachmentLocation(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
categoryId: 1,
|
||||||
|
locationCode: 'A01-01-01',
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.locationCode, 'A01-01-01');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveAttachmentBox sends request with category_id', () async {
|
||||||
|
late Map<String, dynamic> body;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
final req = request as http.Request;
|
||||||
|
body = jsonDecode(req.body) as Map<String, dynamic>;
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'box_item_id': 1,
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 10,
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'category_id': 1,
|
||||||
|
'quantity': 30,
|
||||||
|
'created_at': '2026-08-12T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.saveAttachmentBox(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
categoryId: 1,
|
||||||
|
boxNo: 10,
|
||||||
|
quantity: 30,
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.boxItemId, 1);
|
||||||
|
expect(body['category_id'], 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveAttachmentBox returns duplicate on 409', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'DUPLICATE_BOX_NO',
|
||||||
|
'message': '排产号 W00009 下箱号 10 已存在',
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 10,
|
||||||
|
}),
|
||||||
|
409,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.saveAttachmentBox(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
categoryId: 1,
|
||||||
|
boxNo: 10,
|
||||||
|
quantity: 30,
|
||||||
|
);
|
||||||
|
expect(result.success, isFalse);
|
||||||
|
expect(result.isDuplicate, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateAttachmentBox sends PATCH with box_no and quantity', () async {
|
||||||
|
late Map<String, dynamic> body;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
final req = request as http.Request;
|
||||||
|
body = jsonDecode(req.body) as Map<String, dynamic>;
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'box_item_id': 1,
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 11,
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'category_id': 1,
|
||||||
|
'quantity': 20,
|
||||||
|
'updated_at': '2026-08-12T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.updateAttachmentBox(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 1,
|
||||||
|
boxNo: 11,
|
||||||
|
quantity: 20,
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(body, {'box_no': 11, 'quantity': 20});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteAttachmentBox handles success and not found', () async {
|
||||||
|
var calls = 0;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
calls += 1;
|
||||||
|
if (calls == 1) {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({'box_item_id': 1, 'deleted': true}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'BOX_ITEM_NOT_FOUND',
|
||||||
|
'message': '指定装箱明细不存在',
|
||||||
|
}),
|
||||||
|
404,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final ok = await svc.deleteAttachmentBox(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 1,
|
||||||
|
);
|
||||||
|
final missing = await svc.deleteAttachmentBox(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 999,
|
||||||
|
);
|
||||||
|
expect(ok.success, isTrue);
|
||||||
|
expect(missing.success, isFalse);
|
||||||
|
expect(missing.errorMessage, '指定装箱明细不存在');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MockClient extends http.BaseClient {
|
class _MockClient extends http.BaseClient {
|
||||||
|
|||||||
Reference in New Issue
Block a user