feat(firstleg): support customer price books

This commit is contained in:
2026-09-08 17:42:20 +08:00
parent 60a41ec3ea
commit b0fd797cd2
21 changed files with 145 additions and 59 deletions
@@ -25,11 +25,14 @@ class PriceBookQueryServiceTest {
void delegatesStableReadOperationsToTheOutputPort() {
ChannelPrefixRow prefix = new ChannelPrefixRow("SEA", "IN", "OUT");
when(reader.findActiveVersionId()).thenReturn(9L);
when(reader.findEffectiveVersionId(10001L)).thenReturn(10L);
when(reader.findChannelPrefix("SEA")).thenReturn(prefix);
assertThat(service.activeVersionId()).isEqualTo(9L);
assertThat(service.effectiveVersionId(10001L)).isEqualTo(10L);
assertThat(service.channelPrefix("SEA")).isSameAs(prefix);
verify(reader).findActiveVersionId();
verify(reader).findEffectiveVersionId(10001L);
verify(reader).findChannelPrefix("SEA");
}
@@ -122,7 +122,7 @@ class FbaPricingServiceTest {
inbound.setCarrierId(null);
inbound.setCarrierName(null);
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("KG", "167.017", "2.345678", "391.77", "10.00"));
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
@@ -164,7 +164,7 @@ class FbaPricingServiceTest {
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
when(measurementRepository.listItems(TENANT, 301L)).thenReturn(List.of(item));
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("CBM", "1.0019", "100", "100.19", "0"));
service.calculate(TENANT, OPERATOR, INBOUND, NOW,
@@ -202,7 +202,7 @@ class FbaPricingServiceTest {
InboundPricingEntity inbound = inbound("200.000", "0.2000");
inbound.setCustomerId(null);
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("KG", "200.000", "2.000000", "400.00", "0.00"));
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-no-customer"))
@@ -223,7 +223,7 @@ class FbaPricingServiceTest {
rule.setId(601L); rule.setRuleCode("CBM-RULE"); rule.setRuleVersion(1);
rule.setChargeMode("CBM"); rule.setDefaultRule(true);
when(repository.matchRules(eq(inbound), any())).thenReturn(List.of(rule));
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("CBM", "1.0001", "100.000000", "100.01", "0.00"));
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
@@ -245,7 +245,7 @@ class FbaPricingServiceTest {
InboundPricingEntity inbound = inbound("100.000", "1.0000");
inbound.setProcessStatus("WAIT_SPLIT");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("KG", "167.000", "2.000000", "334.00", "0.00"));
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split");
@@ -304,11 +304,11 @@ class FbaPricingServiceTest {
InboundPricingEntity inbound = inbound("10", "1");
inbound.setMeasureReviewStatus("REJECTED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("CBM", "1.0000", "10", "10", "0"));
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3")).isNotNull();
verify(priceBookEstimator).estimate(any());
verify(priceBookEstimator).estimate(eq(TENANT), any());
}
@Test
@@ -323,7 +323,7 @@ class FbaPricingServiceTest {
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
when(repository.supersedeCurrent(TENANT, 501L, "SENT")).thenReturn(1);
when(priceBookEstimator.estimate(any())).thenReturn(
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("CBM", "1.0000", "10", "10", "0"));
FeeSnapshotResponse response = service.calculate(
@@ -365,7 +365,7 @@ class FbaPricingServiceTest {
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenThrow(
when(priceBookEstimator.estimate(anyLong(), any())).thenThrow(
new BizException(ResultCode.NOT_FOUND, "当前条件没有匹配价格"));
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
@@ -29,6 +29,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -44,7 +45,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldResolveNormalAndOverseasInboundCodePrefixesFromChannelCatalog() {
when(repository.activeVersionId()).thenReturn(7L);
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
when(repository.channelAvailable(7L, 10L, "MATSON_CLX_TRUCK")).thenReturn(true);
when(repository.channels(7L, 10L)).thenReturn(List.of(
channel(7L, "MATSON_CLX_TRUCK", PriceModel.FBA_WAREHOUSE_RATE)));
@@ -59,7 +60,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldFallBackToNormalPrefixWhenChannelHasNoOverseasVariant() {
when(repository.activeVersionId()).thenReturn(7L);
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
when(repository.channelAvailable(7L, 10L, "OA_HOUSTON_TRUCK")).thenReturn(true);
when(repository.channels(7L, 10L)).thenReturn(List.of(
channel(7L, "OA_HOUSTON_TRUCK", PriceModel.FBA_WAREHOUSE_RATE)));
@@ -72,7 +73,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldValidateLegacyChannelWithoutRequiringAnInboundCodePrefix() {
when(repository.activeVersionId()).thenReturn(7L);
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
when(repository.channelAvailable(7L, 10L, "LEGACY_CHANNEL")).thenReturn(true);
var response = service.requireAvailableChannel(
10001L, 10L, "LEGACY_CHANNEL", "FBA", false);
@@ -84,7 +85,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldReturnEmptyChannelsWithoutActiveVersion() {
when(repository.activeVersionId()).thenReturn(null);
when(repository.effectiveVersionId(anyLong())).thenReturn(null);
assertThat(service.channels(10001L, 10L)).isEmpty();
verify(repository, never()).channels(1L, 10L);
@@ -93,7 +94,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldExposeOnlyChannelsFromLatestActiveVersionAfterSwitch() {
when(repository.activeVersionId()).thenReturn(1L, 2L);
when(repository.effectiveVersionId(anyLong())).thenReturn(1L, 2L);
when(repository.channels(1L, 10L)).thenReturn(List.of(channel(1L, "OLD", PriceModel.FBA_WAREHOUSE_RATE)));
when(repository.channels(2L, 10L)).thenReturn(List.of(channel(2L, "NEW", PriceModel.FBA_WAREHOUSE_RATE)));
@@ -105,7 +106,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldExposeDistinctPricingTaxModesForActiveOriginChannels() {
when(repository.activeVersionId()).thenReturn(7L);
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
when(repository.channels(7L, 10L)).thenReturn(List.of(
channel(7L, "MATSON_CLX_TRUCK", PriceModel.FBA_WAREHOUSE_RATE),
channel(7L, "OA_PARCEL", PriceModel.REGION_WEIGHT_TIER)));
@@ -124,7 +125,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldKeepActiveChannelWhenItsTaxModeProjectionIsEmpty() {
when(repository.activeVersionId()).thenReturn(7L);
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
when(repository.channels(7L, 10L)).thenReturn(List.of(
channel(7L, "MATSON_CLX_TRUCK", PriceModel.FBA_WAREHOUSE_RATE)));
when(repository.channelTaxModes(7L, 10L)).thenReturn(List.of());
@@ -136,7 +137,7 @@ class MerchantPriceBookServiceTest {
@Test
void shouldRejectForgedOrRemovedChannel() {
when(repository.activeVersionId()).thenReturn(2L);
when(repository.effectiveVersionId(anyLong())).thenReturn(2L);
when(repository.channels(2L, 10L)).thenReturn(List.of(channel(2L, "ACTIVE_CHANNEL",
PriceModel.FBA_WAREHOUSE_RATE)));
@@ -282,7 +283,7 @@ class MerchantPriceBookServiceTest {
}
private void stubChannel(long versionId, String code, PriceModel model) {
when(repository.activeVersionId()).thenReturn(versionId);
when(repository.effectiveVersionId(anyLong())).thenReturn(versionId);
when(repository.channels(versionId, 10L)).thenReturn(List.of(channel(versionId, code, model)));
}
@@ -18,6 +18,7 @@ import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -48,7 +49,7 @@ class OrderChannelChangePricingServiceTest {
InboundPricingEntity inbound = inbound("CALCULATED");
when(repository.findInboundByOrder(10001L, 77L)).thenReturn(inbound);
when(repository.findLatestFee(10001L, 88L)).thenReturn(fee(91L, "CALCULATED"));
when(estimator.estimate(any())).thenReturn(estimate());
when(estimator.estimate(anyLong(), any())).thenReturn(estimate());
var response = service.validate(request(4));
@@ -64,7 +65,7 @@ class OrderChannelChangePricingServiceTest {
InboundFeeEntity fee = fee(91L, "CALCULATED");
when(repository.findInboundByOrderForUpdate(10001L, 77L)).thenReturn(inbound);
when(repository.findLatestFeeForUpdate(10001L, 88L)).thenReturn(fee);
when(estimator.estimate(any())).thenReturn(estimate());
when(estimator.estimate(anyLong(), any())).thenReturn(estimate());
when(repository.supersedeCurrent(10001L, 91L, "CALCULATED")).thenReturn(1);
when(repository.applyOrderChannelChange(inbound, "MATSON_NEW", "新渠道")).thenReturn(1);
when(repository.completeIdempotency(any(), org.mockito.ArgumentMatchers.eq(4L), any())).thenReturn(1);
@@ -88,7 +89,7 @@ class OrderChannelChangePricingServiceTest {
token.setRequestHash(requestHash(request(4)));
when(repository.findInboundByOrderForUpdate(10001L, 77L)).thenReturn(inbound);
when(repository.findIdempotency(10001L, 0L, "ORDER_CHANNEL_CHANGE", "77:4")).thenReturn(token);
when(estimator.estimate(any())).thenReturn(estimate());
when(estimator.estimate(anyLong(), any())).thenReturn(estimate());
assertThat(service.apply(request(4)).orderVersion()).isEqualTo(4L);
@@ -64,6 +64,22 @@ class PlatformPriceBookServiceTest {
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
}
@Test
void listsCustomerSpecificVersionsWithinRequestedScope() {
PriceBookVersionEntity customerVersion = version(21L, PriceBookStatus.ACTIVE);
customerVersion.setTenantId(10001L);
when(repository.listVersions(10001L, PriceBookStatus.ACTIVE.name()))
.thenReturn(List.of(customerVersion));
var result = service.list(10001L, PriceBookStatus.ACTIVE);
assertThat(result).singleElement().satisfies(summary -> {
assertThat(summary.id()).isEqualTo(21L);
assertThat(summary.customerId()).isEqualTo(10001L);
});
verify(repository).listVersions(10001L, PriceBookStatus.ACTIVE.name());
}
@Test
void fileServiceFailureMarksAllocatedVersionFailed() {
executeTransactionsImmediately();
@@ -121,7 +137,7 @@ class PlatformPriceBookServiceTest {
assertThat(Arrays.stream(PlatformPriceBookService.class.getDeclaredMethods())
.filter(method -> java.lang.reflect.Modifier.isPublic(method.getModifiers()))
.map(java.lang.reflect.Method::getName).toList())
.containsExactlyInAnyOrder("list", "upload", "detail", "publish", "source");
.containsOnly("list", "upload", "detail", "publish", "source");
}
@Test