This commit is contained in:
2026-08-25 12:27:47 +08:00
parent 0e0b6f8264
commit 4b5e5a1e4e
13 changed files with 183 additions and 33 deletions
@@ -9,7 +9,7 @@ paths:
summary: List official carrier sources and compliant access modes
x-permission: platform:firstleg:port-schedule-query:list
responses:
'200': {description: MATSON, SML, ZIM, COSCO, EMC and HEDE descriptors}
'200': {description: 'MATSON, SML, ZIM, COSCO, EMC and HEDE descriptors'}
/api/platform/v1/carrier-schedule-queries:
get:
summary: Query carrier schedule tasks
@@ -18,7 +18,7 @@ paths:
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/CustomerRuleVersionSummary'}
schema: {$ref: '#/components/schemas/CustomerRuleVersionListApiResponse'}
'403': {description: 无权限}
/api/platform/v1/fba-pricing/customer-rules/versions:
post:
@@ -231,6 +231,44 @@ components:
VersionId: {name: versionId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
schemas:
CustomerRuleVersionListApiResponse:
type: object
required: [code, message, data, timestamp]
properties:
code: {type: string, example: '200'}
message: {type: string}
data:
type: array
items: {$ref: '#/components/schemas/CustomerRuleVersionResource'}
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
timestamp: {type: string, format: date-time}
CustomerRuleVersionResource:
type: object
properties:
id: {type: string, pattern: '^[1-9][0-9]*$'}
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
ruleCode: {type: string}
ruleVersion: {type: integer}
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
customerName: {type: string}
businessType: {type: string}
chargeMode: {type: string}
taxMode: {type: string}
conversionFactor: {type: number}
roundingScale: {type: integer}
roundingMode: {type: string}
currency: {type: string}
effectiveFrom: {type: string, format: date-time}
effectiveTo: {type: string, format: date-time, nullable: true}
status: {type: string}
remark: {type: string, nullable: true}
createdBy: {type: string, nullable: true}
createdAt: {type: string, format: date-time, nullable: true}
updatedBy: {type: string, nullable: true}
updatedAt: {type: string, format: date-time, nullable: true}
deleted: {type: boolean, nullable: true}
version: {type: integer, nullable: true}
FeeSnapshotApiResponse:
type: object
required: [code, message, data]
@@ -415,7 +453,7 @@ components:
required: [expectedFeeVersion, chargeableQuantity, unitPrice, additionalAmount, reason]
properties:
expectedFeeVersion: {type: integer, minimum: 1}
chargeableQuantity: {type: number, exclusiveMinimum: 0, description: 计费量,单位沿用当前费用快照的 chargeUnit}
chargeableQuantity: {type: number, minimum: 0, exclusiveMinimum: true, description: 计费量,单位沿用当前费用快照的 chargeUnit}
unitPrice: {type: number, minimum: 0, description: 人工确认单价}
additionalAmount: {type: number, minimum: 0, multipleOf: 0.01}
reason: {type: string, minLength: 1, maxLength: 500}
@@ -432,7 +470,7 @@ components:
businessType: {type: string, maxLength: 32}
chargeMode: {type: string, enum: [KG, CBM]}
taxMode: {type: string, enum: [TAX_INCLUDED, TAX_EXCLUDED]}
conversionFactor: {type: number, exclusiveMinimum: 0, example: 167}
conversionFactor: {type: number, minimum: 0, exclusiveMinimum: true, example: 167}
roundingScale: {type: integer, minimum: 0, maximum: 4}
roundingMode: {type: string, enum: [UP, DOWN, CEILING, FLOOR, HALF_UP, HALF_DOWN, HALF_EVEN]}
currency: {type: string, pattern: '^[A-Z]{3}$'}
@@ -454,7 +492,7 @@ components:
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
destinationName: {type: string, maxLength: 128, nullable: true}
chargeUnit: {type: string, enum: [KG, CBM]}
unitPrice: {type: number, exclusiveMinimum: 0}
unitPrice: {type: number, minimum: 0, exclusiveMinimum: true}
currency: {type: string, pattern: '^[A-Z]{3}$'}
additionalFee: {type: number, minimum: 0}
effectiveFrom: {type: string, format: date-time}
@@ -578,7 +616,7 @@ components:
pricingTaxMode: {$ref: '#/components/schemas/PricingTaxMode'}
chargeUnit: {type: string, enum: [KG, CBM]}
weightFromKg: {type: number, minimum: 0, nullable: true}
unitPrice: {type: number, exclusiveMinimum: 0}
unitPrice: {type: number, minimum: 0, exclusiveMinimum: true}
currency: {type: string, pattern: '^[A-Z]{3}$'}
sourceSheetName: {type: string, maxLength: 128}
sourceRowNumber: {type: integer, minimum: 1}
@@ -1112,7 +1112,6 @@ components:
notificationStatus: {type: string, nullable: true}
version: {type: integer, nullable: true}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
requestedPickupAt: {type: string, format: date-time, nullable: true, description: Customer-requested pickup appointment from fl_inbound_pickup_snapshot.pickup_at}
customerId: {type: integer, format: int64, nullable: true}
customerName: {type: string, nullable: true}
companyId: {type: integer, format: int64, nullable: true}
@@ -34,7 +34,7 @@ paths:
content:
application/json: {schema: {$ref: '#/components/schemas/UpsertEvidenceRequest'}}
responses:
'200': {description: Business envelope; ownership, type and version failures use business error codes}
'200': {description: 'Business envelope; ownership, type and version failures use business error codes'}
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/content/pda:
get:
summary: Download one loading evidence photo for the active PDA worker
@@ -93,7 +93,7 @@ paths:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/BatchRequest'}}}
responses:
'200': {description: Completed task, identical replay result, or A0409 when any snapshot changed}
'200': {description: 'Completed task, identical replay result, or A0409 when any snapshot changed'}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/review:
post:
summary: 人工确认装柜完成的兼容入口
@@ -8,7 +8,7 @@ paths:
get:
summary: Query truck appointments
parameters:
- {name: customerId, in: query, description: Filter by customer, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: customerId, in: query, description: 'Filter by customer', schema: {type: string, pattern: '^[1-9][0-9]*$'}}
responses:
'200':
description: Paged truck appointments with stable booking/container snapshots and attachment count
@@ -29,13 +29,13 @@ paths:
application/json:
schema: {$ref: '#/components/schemas/SaveRequest'}
responses:
'200': {description: Created appointment, content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}}}
'200': {description: 'Created appointment', content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}}}
/api/platform/v1/truck-appointments/{id}:
parameters: [{$ref: '#/components/parameters/AppointmentId'}]
get:
summary: Query a truck appointment
responses:
'200': {description: Appointment detail, content: {application/json: {schema: {$ref: '#/components/schemas/DetailStableReferences'}}}}
'200': {description: 'Appointment detail', content: {application/json: {schema: {$ref: '#/components/schemas/DetailStableReferences'}}}}
put:
summary: Update an editable truck appointment with optimistic locking
requestBody:
@@ -84,20 +84,20 @@ paths:
get:
summary: Query immutable appointment status history
responses:
'200': {description: History entries including action, from/to status, operator, reason and time}
'200': {description: 'History entries including action, from/to status, operator, reason and time'}
/api/platform/v1/truck-appointments/{id}/attachments:
parameters: [{$ref: '#/components/parameters/AppointmentId'}]
get:
summary: Query attachment references owned by the appointment
description: 'Requires platform:firstleg:truck-appointment:list.'
responses:
'200': {description: Attachment references, content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/Attachment'}}}}}
'200': {description: 'Attachment references', content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/Attachment'}}}}}
post:
summary: Reference a file already owned by the appointment customer and business scope
description: 'Requires platform:firstleg:truck-appointment:edit. The file must belong to customerId + TRUCK_APPOINTMENT_ATTACHMENT + appointmentId.'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/AttachmentRequest'}}}}
responses:
'200': {description: Created attachment reference, content: {application/json: {schema: {$ref: '#/components/schemas/Attachment'}}}}
'200': {description: 'Created attachment reference', content: {application/json: {schema: {$ref: '#/components/schemas/Attachment'}}}}
/api/platform/v1/truck-appointments/{id}/attachments/upload:
parameters: [{$ref: '#/components/parameters/AppointmentId'}]
post:
@@ -181,8 +181,8 @@ components:
required: [customerId, carrierId, plannedLoadingAt, expectedVersion]
properties:
outboundOrderId: {type: integer, format: int64, minimum: 1, nullable: true}
oceanBookingId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional booking association; no RELEASED-state prerequisite. When supplied, customer and warehouse must match.}
containerBookingId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional ACTIVE container association. When supplied, customer and warehouse must match.}
oceanBookingId: {type: integer, format: int64, minimum: 1, nullable: true, description: 'Optional booking association; no RELEASED-state prerequisite. When supplied, customer and warehouse must match.'}
containerBookingId: {type: integer, format: int64, minimum: 1, nullable: true, description: 'Optional ACTIVE container association. When supplied, customer and warehouse must match.'}
loadingPlanId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional loading-plan association. It may be supplied only with containerBookingId and must be that container's current plan.}
warehouseId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional warehouse scope. Linked booking/container warehouse facts must agree; when omitted the linked fact is frozen if available.}
customerId: {type: integer, format: int64, minimum: 1, description: Required ACTIVE IAM customer used for isolation and attachment ownership.}
@@ -4,9 +4,12 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.RequestParam;
import org.yaml.snakeyaml.Yaml;
import java.nio.charset.StandardCharsets;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -82,9 +85,57 @@ class FbaPricingContractTest {
.doesNotContain("format: int64");
}
@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");
}
private static Method method(String name) {
return java.util.Arrays.stream(PlatformFbaMasterDataController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name))
.findFirst().orElseThrow();
}
@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;
}
}