feat(firstleg): expose loading item tax mode

This commit is contained in:
2026-09-08 17:07:06 +08:00
parent 40fe0ba0e1
commit 78a73a1b0e
7 changed files with 74 additions and 4 deletions
@@ -177,6 +177,8 @@ public final class LoadingListDtos {
Long itemId, Long itemId,
Long accountId, Long accountId,
String customerName, String customerName,
String customsTaxMode,
Boolean taxRefundFlag,
Long orderId, Long orderId,
String orderNo, String orderNo,
Long inboundId, Long inboundId,
@@ -168,7 +168,14 @@ public interface LoadingListQueryMapper {
@Param("operatorName") String operatorName); @Param("operatorName") String operatorName);
@Select(""" @Select("""
SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no, SELECT i.id AS item_id, i.account_id, i.customer_name,
inbound.tax_mode AS customs_tax_mode,
CASE inbound.tax_mode
WHEN 'TAX_REFUND' THEN TRUE
WHEN 'BUY_DECLARATION' THEN FALSE
ELSE NULL
END AS tax_refund_flag,
i.order_id, i.order_no,
i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no, i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no,
i.sku_code, i.destination_warehouse_name, i.product_name_zh, i.sku_code, i.destination_warehouse_name, i.product_name_zh,
product_name_en, product_image_url, i.package_count, product_name_en, product_image_url, i.package_count,
@@ -409,6 +416,8 @@ public interface LoadingListQueryMapper {
private Long itemId; private Long itemId;
private Long accountId; private Long accountId;
private String customerName; private String customerName;
private String customsTaxMode;
private Boolean taxRefundFlag;
private Long orderId; private Long orderId;
private String orderNo; private String orderNo;
private Long inboundId; private Long inboundId;
@@ -184,7 +184,8 @@ public class LoadingListRepository {
private List<Item> items(List<ItemRow> rows) { private List<Item> items(List<ItemRow> rows) {
return rows.stream().map(row -> new Item(row.getItemId(), row.getAccountId(), row.getCustomerName(), return rows.stream().map(row -> new Item(row.getItemId(), row.getAccountId(), row.getCustomerName(),
row.getOrderId(), row.getOrderNo(), row.getInboundId(), row.getInboundDetailId(), row.getCustomsTaxMode(), row.getTaxRefundFlag(), row.getOrderId(), row.getOrderNo(),
row.getInboundId(), row.getInboundDetailId(),
row.getInboundCode(), row.getContainerNo(), row.getSkuCode(), row.getInboundCode(), row.getContainerNo(), row.getSkuCode(),
row.getDestinationWarehouseName(), row.getProductNameZh(), row.getProductNameEn(), row.getDestinationWarehouseName(), row.getProductNameZh(), row.getProductNameEn(),
row.getProductImageUrl(), integer(row.getPackageCount()), integer(row.getPalletCount()), row.getProductImageUrl(), integer(row.getPackageCount()), integer(row.getPalletCount()),
@@ -361,6 +361,8 @@ components:
itemId: {$ref: '#/components/schemas/LongId'} itemId: {$ref: '#/components/schemas/LongId'}
accountId: {$ref: '#/components/schemas/LongId'} accountId: {$ref: '#/components/schemas/LongId'}
customerName: {type: string, nullable: true, description: 客户名称快照} customerName: {type: string, nullable: true, description: 客户名称快照}
customsTaxMode: {type: string, enum: [TAX_REFUND, BUY_DECLARATION], nullable: true, description: 订单/入仓片段冻结的报关方式;不根据退税文件推断}
taxRefundFlag: {type: boolean, nullable: true, description: customsTaxMode为TAX_REFUND时为true、BUY_DECLARATION时为false;缺少权威事实时为null}
orderId: {$ref: '#/components/schemas/LongId'} orderId: {$ref: '#/components/schemas/LongId'}
orderNo: {type: string, description: 订单号快照} orderNo: {type: string, description: 订单号快照}
inboundId: {$ref: '#/components/schemas/LongId'} inboundId: {$ref: '#/components/schemas/LongId'}
@@ -16,6 +16,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent; import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map; import java.util.Map;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -145,9 +146,15 @@ class LoadingListControllerContractTest {
.doesNotContainKey("internalContainerNo"); .doesNotContainKey("internalContainerNo");
assertThat(properties(schemas, "ConfirmedSummary")) assertThat(properties(schemas, "ConfirmedSummary"))
.containsKeys("planId", "containerBookingId", "loadingListNo"); .containsKeys("planId", "containerBookingId", "loadingListNo");
assertThat(properties(schemas, "LoadingItem")).containsKeys( Map<String, Object> loadingItem = properties(schemas, "LoadingItem");
"itemId", "accountId", "inboundId", "inboundDetailId", "skuCode", assertThat(loadingItem).containsKeys(
"itemId", "accountId", "customsTaxMode", "taxRefundFlag", "inboundId", "inboundDetailId", "skuCode",
"destinationWarehouse", "productQuantity", "frozenAt"); "destinationWarehouse", "productQuantity", "frozenAt");
assertThat(stringMap(loadingItem.get("customsTaxMode")))
.containsEntry("nullable", true)
.containsEntry("enum", List.of("TAX_REFUND", "BUY_DECLARATION"));
assertThat(stringMap(loadingItem.get("taxRefundFlag")))
.containsEntry("nullable", true);
assertThat(properties(schemas, "OperationHistory")).containsKeys( assertThat(properties(schemas, "OperationHistory")).containsKeys(
"historyId", "itemId", "operationId", "actionType", "fromStatus", "toStatus", "historyId", "itemId", "operationId", "actionType", "fromStatus", "toStatus",
"beforeSnapshot", "afterSnapshot", "operatorId", "occurredAt"); "beforeSnapshot", "afterSnapshot", "operatorId", "occurredAt");
@@ -59,6 +59,11 @@ class LoadingListQueryMapperContractTest {
String items = sql("selectItems", Select.class); String items = sql("selectItems", Select.class);
assertThat(items) assertThat(items)
.contains("i.order_id", "i.order_no", "b.container_no") .contains("i.order_id", "i.order_no", "b.container_no")
.contains("inbound.tax_mode AS customs_tax_mode")
.contains("WHEN 'TAX_REFUND' THEN TRUE")
.contains("WHEN 'BUY_DECLARATION' THEN FALSE")
.contains("ELSE NULL")
.doesNotContain("fl_customs_container_document", "fl_customs_container_document_link")
.contains("b.record_status='ACTIVE'") .contains("b.record_status='ACTIVE'")
.contains("NULLIF(i.pallet_count,0)") .contains("NULLIF(i.pallet_count,0)")
.contains("fl_inbound_weight_result weight") .contains("fl_inbound_weight_result weight")
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery; import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper; import com.baiyue.firstleg.mapper.LoadingListQueryMapper;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow; import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.ItemRow;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -96,4 +97,47 @@ class LoadingListRepositoryTest {
var confirmed = repository.preloadDetail(row).summary().readiness(); var confirmed = repository.preloadDetail(row).summary().readiness();
assertThat(confirmed.readyForPda()).isTrue(); assertThat(confirmed.readyForPda()).isTrue();
} }
@Test
void mapsFrozenCustomsTaxFactsPerInboundAcrossCustomers() {
AggregateRow aggregate = new AggregateRow();
aggregate.setPlanId(41L);
aggregate.setContainerBookingId(61L);
aggregate.setPlanNo("LP-41");
aggregate.setPlanStatus("COMPLETED");
aggregate.setVersion(3);
ItemRow refund = item(101L, 11L, 201L, "IN-REFUND", "TAX_REFUND", true);
ItemRow declaration = item(102L, 12L, 202L, "IN-BUY", "BUY_DECLARATION", false);
ItemRow unknown = item(103L, 12L, 203L, "IN-UNKNOWN", null, null);
when(mapper.selectItems(41L)).thenReturn(List.of(refund, declaration, unknown));
when(mapper.selectHistories(41L)).thenReturn(List.of());
var items = repository.confirmedDetail(aggregate).items();
assertThat(items).extracting(value -> value.inboundCode(), value -> value.accountId(),
value -> value.customsTaxMode(), value -> value.taxRefundFlag())
.containsExactly(
org.assertj.core.groups.Tuple.tuple("IN-REFUND", 11L, "TAX_REFUND", true),
org.assertj.core.groups.Tuple.tuple("IN-BUY", 12L, "BUY_DECLARATION", false),
org.assertj.core.groups.Tuple.tuple("IN-UNKNOWN", 12L, null, null));
}
private static ItemRow item(long itemId, long accountId, long inboundId, String inboundCode,
String customsTaxMode, Boolean taxRefundFlag) {
ItemRow row = new ItemRow();
row.setItemId(itemId);
row.setAccountId(accountId);
row.setCustomerName("customer-" + accountId);
row.setCustomsTaxMode(customsTaxMode);
row.setTaxRefundFlag(taxRefundFlag);
row.setOrderId(300L + inboundId);
row.setOrderNo("ORDER-" + inboundId);
row.setInboundId(inboundId);
row.setInboundDetailId(400L + inboundId);
row.setInboundCode(inboundCode);
row.setPackageCount(1);
row.setLoadSequence(1);
row.setItemVersion(0);
return row;
}
} }