Files
baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java
T

181 lines
9.5 KiB
Java
Raw Normal View History

2026-08-04 10:07:45 +08:00
package com.baiyue.firstleg.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
2026-09-03 10:44:34 +08:00
import org.springframework.security.access.prepost.PreAuthorize;
2026-08-05 09:11:26 +08:00
import org.springframework.web.bind.annotation.RequestParam;
2026-08-25 12:27:47 +08:00
import org.yaml.snakeyaml.Yaml;
2026-08-04 10:07:45 +08:00
import java.nio.charset.StandardCharsets;
2026-08-05 09:11:26 +08:00
import java.lang.reflect.Method;
2026-08-25 12:27:47 +08:00
import java.util.List;
import java.util.Map;
2026-08-04 10:07:45 +08:00
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("FBA计费OpenAPI契约")
class FbaPricingContractTest {
2026-09-03 10:44:34 +08:00
@Test
@DisplayName("确认和发送费用兼容新的组合权限与原有权限")
void allowsCombinedConfirmSendPermissionAndLegacyPermissions() {
assertPricingAuthority("confirm", "hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
+ " 'firstleg:fba-pricing:confirm')");
assertPricingAuthority("send", "hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
+ " 'firstleg:inbound:send-fee')");
2026-09-09 17:58:07 +08:00
assertPricingAuthority("adjustPublished", "hasAnyAuthority('firstleg:fba-pricing:adjust',"
+ " 'firstleg:inbound:confirm-send-fee')");
2026-09-21 10:44:23 +08:00
assertPricingAuthority("manualQuote", "hasAuthority('firstleg:inbound:confirm-send-fee')");
2026-09-03 10:44:34 +08:00
}
2026-08-05 09:11:26 +08:00
@Test
2026-08-06 18:01:59 +08:00
@DisplayName("客户规则保留客户参数,船司价格使用平台公共作用域")
void keepsCustomerRuleScopeAndRemovesCarrierRateTenantScope() {
Method rules = method("rules");
assertThat(rules.getParameterTypes()[1]).isEqualTo(Long.class);
assertThat(rules.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
2026-08-05 09:11:26 +08:00
2026-08-06 18:01:59 +08:00
Method rates = method("rates");
assertThat(rates.getParameterTypes()).doesNotContain(Long.class);
2026-09-03 10:44:34 +08:00
for (String scopedMethod : new String[] {"rule", "createRule", "retireRule", "setDefaultRule"}) {
2026-08-05 09:11:26 +08:00
Method method = method(scopedMethod);
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
}
2026-08-06 18:01:59 +08:00
for (String publicMethod : new String[] {"rate", "createRate", "retireRate"}) {
assertThat(java.util.Arrays.stream(method(publicMethod).getParameters())
.map(parameter -> parameter.getAnnotation(RequestParam.class)))
.allMatch(annotation -> annotation == null);
}
2026-08-05 09:11:26 +08:00
}
2026-08-04 10:07:45 +08:00
@Test
2026-08-14 14:26:56 +08:00
@DisplayName("契约声明二期边界计费、人工费用调整和幂等头")
2026-08-04 10:07:45 +08:00
void documentsPricingLifecycle() throws Exception {
String openapi = new ClassPathResource("openapi/fba-pricing-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi)
.contains("/api/platform/v1/fba-pricing/customer-rules/versions:")
2026-09-03 10:44:34 +08:00
.contains("/api/platform/v1/fba-pricing/customer-rules/{id}/default:")
2026-08-04 10:07:45 +08:00
.contains("/api/platform/v1/fba-pricing/carrier-rates/versions:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/calculate:")
2026-09-21 10:44:23 +08:00
.contains("/api/platform/v1/inbounds/{inboundId}/fees/manual-quote:")
2026-08-04 10:07:45 +08:00
.contains("/api/platform/v1/inbounds/{inboundId}/fees/confirm:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
.contains("Idempotency-Key")
2026-08-13 14:37:51 +08:00
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态")
2026-08-14 12:50:37 +08:00
.contains("区域阶梯渠道可按平台 FBA 仓或海外中转仓的邮编区域")
.contains("不再以 carrierId、业务 customerId 或客户计费规则作为前置")
2026-08-17 19:13:08 +08:00
.contains("仅此动作将费用置为 SENT")
.contains("写入 Finance Outbox")
2026-08-13 14:37:51 +08:00
.contains("不回退 WAIT_SPLIT 状态")
2026-08-14 14:26:56 +08:00
.contains("expectedFeeVersion")
.contains("chargeableQuantity")
.contains("unitPrice")
.doesNotContain("required: [subtotalAmount, additionalAmount, reason]")
2026-08-04 10:07:45 +08:00
.doesNotContain("待客户确认");
2026-08-05 09:11:26 +08:00
String customerRules = openapi.substring(
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules:"),
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules/versions:"));
assertThat(customerRules)
.contains("OptionalTenantId")
.contains("name: ruleCode")
.doesNotContain("#/components/parameters/Page")
.doesNotContain("#/components/parameters/Size")
.doesNotContain("name: customerName")
.doesNotContain("name: chargeMode")
.doesNotContain("name: status");
2026-08-05 09:30:08 +08:00
String carrierRates = openapi.substring(
openapi.indexOf("/api/platform/v1/fba-pricing/carrier-rates:"),
openapi.indexOf("/api/platform/v1/fba-pricing/carrier-rates/versions:"));
assertThat(carrierRates)
2026-08-06 18:01:59 +08:00
.doesNotContain("OptionalTenantId")
2026-08-05 09:30:08 +08:00
.contains("name: rateCode");
2026-08-05 09:11:26 +08:00
assertThat(openapi)
.contains("OptionalTenantId: {name: tenantId, in: query, required: false")
.contains("schema: {type: string, pattern: '^[1-9][0-9]*$'}")
.doesNotContain("format: int64");
}
@Test
@DisplayName("费用快照强制返回实际换算与计费量舍入参数")
void requiresActualConversionAndRoundingMetadata() throws Exception {
String source = new ClassPathResource("openapi/fba-pricing-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> schemas = map(map(new Yaml().load(source)).get("components"));
Map<String, Object> fee = map(map(schemas.get("schemas")).get("FeeSnapshotResponse"));
assertThat(list(fee.get("required"))).contains(
"conversionFactor", "roundingScale", "roundingMode");
Map<String, Object> properties = map(fee.get("properties"));
assertThat(map(properties.get("conversionFactor")).get("description").toString())
.contains("167", "363");
assertThat(list(map(properties.get("roundingScale")).get("enum"))).containsExactly(6);
assertThat(list(map(properties.get("roundingMode")).get("enum"))).containsExactly("HALF_UP");
}
2026-08-25 12:27:47 +08:00
@Test
@DisplayName("客户规则列表响应锁定数组包络和实体核心字段")
void freezesCustomerRuleVersionListResponse() throws Exception {
String source = new ClassPathResource("openapi/fba-pricing-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> api = map(new Yaml().load(source));
Map<String, Object> paths = map(api.get("paths"));
Map<String, Object> get = map(map(paths.get(
"/api/platform/v1/fba-pricing/customer-rules")).get("get"));
Map<String, Object> success = map(map(get.get("responses")).get("200"));
Map<String, Object> responseSchema = map(map(map(success.get("content"))
.get("application/json")).get("schema"));
assertThat(responseSchema).containsEntry(
"$ref", "#/components/schemas/CustomerRuleVersionListApiResponse");
Map<String, Object> schemas = map(map(api.get("components")).get("schemas"));
Map<String, Object> envelope = map(schemas.get("CustomerRuleVersionListApiResponse"));
assertThat(list(envelope.get("required"))).containsExactly("code", "message", "data", "timestamp");
Map<String, Object> envelopeProperties = map(envelope.get("properties"));
assertThat(envelopeProperties).containsOnlyKeys(
"code", "message", "data", "requestId", "traceId", "timestamp");
Map<String, Object> data = map(envelopeProperties.get("data"));
assertThat(data).containsEntry("type", "array");
assertThat(map(data.get("items"))).containsEntry(
"$ref", "#/components/schemas/CustomerRuleVersionResource");
Map<String, Object> resource = map(schemas.get("CustomerRuleVersionResource"));
assertThat(map(resource.get("properties"))).containsOnlyKeys(
"id", "tenantId", "ruleCode", "ruleVersion", "customerId", "customerName",
"businessType", "chargeMode", "taxMode", "conversionFactor", "roundingScale",
"roundingMode", "currency", "effectiveFrom", "effectiveTo", "status", "remark",
"createdBy", "createdAt", "updatedBy", "updatedAt", "deleted", "version");
for (String longField : List.of("id", "tenantId", "customerId", "createdBy", "updatedBy")) {
assertThat(map(map(resource.get("properties")).get(longField))).containsEntry("type", "string");
}
assertThat(source).doesNotContain("CustomerRuleVersionSummary");
}
2026-08-05 09:11:26 +08:00
private static Method method(String name) {
return java.util.Arrays.stream(PlatformFbaMasterDataController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name))
.findFirst().orElseThrow();
2026-08-04 10:07:45 +08:00
}
2026-08-25 12:27:47 +08:00
2026-09-03 10:44:34 +08:00
private static void assertPricingAuthority(String name, String expected) {
Method method = java.util.Arrays.stream(PlatformFbaPricingController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name))
.findFirst().orElseThrow();
assertThat(method.getAnnotation(PreAuthorize.class).value()).isEqualTo(expected);
}
2026-08-25 12:27:47 +08:00
@SuppressWarnings("unchecked")
private static Map<String, Object> map(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<Object> list(Object value) {
return (List<Object>) value;
}
2026-08-04 10:07:45 +08:00
}