merge: sync test and stabilize full acceptance
This commit is contained in:
@@ -187,7 +187,10 @@ public class OrderApplicationService {
|
||||
if (target == OrderStatus.COMPLETED) order.setCompletedAt(LocalDateTime.now());
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
record(tenantId, id, from, target, target.name(), reason, userId, username);
|
||||
if (target == OrderStatus.APPROVED || target == OrderStatus.CANCELLED) writeOutbox(order, target);
|
||||
if (target == OrderStatus.SUBMITTED) prepareSubmittedInbound(order);
|
||||
if (target == OrderStatus.SUBMITTED || target == OrderStatus.APPROVED || target == OrderStatus.CANCELLED) {
|
||||
writeOutbox(order, target);
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交订单的幂等入口。 */
|
||||
@@ -668,9 +671,13 @@ public class OrderApplicationService {
|
||||
private void writeOutbox(OrderEntity order, OrderStatus target) {
|
||||
OutboxEventEntity event = new OutboxEventEntity();
|
||||
event.setTenantId(order.getTenantId()); event.setAggregateType("FIRST_LEG_ORDER"); event.setAggregateId(order.getId());
|
||||
event.setEventType(target == OrderStatus.APPROVED ? "FIRST_LEG_ORDER_APPROVED" : "FIRST_LEG_ORDER_CANCELLED");
|
||||
event.setEventType(switch (target) {
|
||||
case SUBMITTED -> "FIRST_LEG_ORDER_PREPARED";
|
||||
case APPROVED -> "FIRST_LEG_ORDER_APPROVED";
|
||||
default -> "FIRST_LEG_ORDER_CANCELLED";
|
||||
});
|
||||
try {
|
||||
event.setPayloadJson(target == OrderStatus.APPROVED
|
||||
event.setPayloadJson(target == OrderStatus.SUBMITTED || target == OrderStatus.APPROVED
|
||||
? json.writeValueAsString(inboundCommand(order, totalPackageCount(order)))
|
||||
: json.writeValueAsString(Map.of("tenantId", order.getTenantId(), "orderId", order.getId(),
|
||||
"orderNo", order.getOrderNo(), "status", target.name())));
|
||||
@@ -699,10 +706,27 @@ public class OrderApplicationService {
|
||||
order.getFulfillmentType() == null ? "STANDARD" : order.getFulfillmentType(), order.getLxCode());
|
||||
}
|
||||
private void prepareDraftInbound(OrderEntity order, OrderRequests.SaveRequest request) {
|
||||
FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable();
|
||||
if (client == null || !StringUtils.hasText(internalToken)) return;
|
||||
int packageCount = request.cargoes().stream().map(OrderRequests.Cargo::packageCount)
|
||||
.filter(java.util.Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
prepareSubmittedInbound(order, packageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交即准备稳定的预入库单,供货物提前到仓时采集称重和箱规;
|
||||
* 订单审核仍由 Order 独立控制,Firstleg 不得据此推进测量审核或后续履约。
|
||||
*/
|
||||
private void prepareSubmittedInbound(OrderEntity order) {
|
||||
long total = repository.findCargo(order.getTenantId(), order.getId()).stream()
|
||||
.map(OrderCargoEntity::getPackageCount)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.mapToLong(Integer::longValue)
|
||||
.sum();
|
||||
prepareSubmittedInbound(order, total > 0 && total <= 1_000_000 ? (int) total : null);
|
||||
}
|
||||
|
||||
private void prepareSubmittedInbound(OrderEntity order, Integer packageCount) {
|
||||
FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable();
|
||||
if (client == null || !StringUtils.hasText(internalToken)) return;
|
||||
FirstlegInboundClient.OrderApprovedInboundCommand command = inboundCommand(order, packageCount);
|
||||
Runnable prepare = () -> {
|
||||
try {
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 可靠投递订单审核事件;接收端以订单唯一键保证重复投递安全。 */
|
||||
/** 可靠投递订单预入库与审核事件;接收端以订单唯一键保证重复投递安全。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class OrderInboundOutboxPublisher {
|
||||
@@ -38,7 +38,8 @@ public class OrderInboundOutboxPublisher {
|
||||
publishDeclarationUpdate(event);
|
||||
continue;
|
||||
}
|
||||
if (!"FIRST_LEG_ORDER_APPROVED".equals(event.getEventType())) {
|
||||
boolean preparing = "FIRST_LEG_ORDER_PREPARED".equals(event.getEventType());
|
||||
if (!preparing && !"FIRST_LEG_ORDER_APPROVED".equals(event.getEventType())) {
|
||||
failPermanently(event, "unsupported order event");
|
||||
continue;
|
||||
}
|
||||
@@ -73,7 +74,9 @@ public class OrderInboundOutboxPublisher {
|
||||
}
|
||||
}
|
||||
try {
|
||||
ApiResponse<Long> response = client.create(internalToken, command);
|
||||
ApiResponse<Long> response = preparing
|
||||
? client.prepare(internalToken, command)
|
||||
: client.create(internalToken, command);
|
||||
if (response == null || !response.successful()) {
|
||||
throw new IllegalStateException("头程入库服务返回失败");
|
||||
}
|
||||
|
||||
@@ -65,9 +65,7 @@ public class UnifiedOrderQueryService {
|
||||
.collect(Collectors.toMap(OrderEntity::getOrderNo, Function.identity(), (left, right) -> left));
|
||||
List<OrderEntity> selected = orderNos.stream().map(orderByNo::get).filter(Objects::nonNull).toList();
|
||||
List<Long> orderIds = selected.stream().map(OrderEntity::getId).toList();
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = summaries(null,
|
||||
selected.stream().filter(order -> order.getStatus() != OrderStatus.SUBMITTED)
|
||||
.map(OrderEntity::getId).toList());
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = summaries(null, orderIds);
|
||||
Map<Long, String> latestNotes = latestNotes(null, orderIds);
|
||||
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(null, orderIds);
|
||||
Map<Long, Integer> childCounts = childCounts(selected);
|
||||
@@ -87,9 +85,10 @@ public class UnifiedOrderQueryService {
|
||||
List<OrderStatus> statuses = "PENDING_REVIEW".equals(query.mainStatus())
|
||||
? List.of(OrderStatus.SUBMITTED) : ACTIVE_ORDER_STATUSES;
|
||||
PageResult<OrderEntity> source = orders.unifiedPage(tenantId, query, statuses);
|
||||
List<Long> sourceOrderIds = source.records().stream().map(OrderEntity::getId).toList();
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = "PENDING_REVIEW".equals(query.mainStatus())
|
||||
? Map.of()
|
||||
: summaries(tenantId, source.records().stream().map(OrderEntity::getId).toList());
|
||||
? optionalSummaries(tenantId, sourceOrderIds)
|
||||
: summaries(tenantId, sourceOrderIds);
|
||||
Map<Long, String> latestNotes = latestNotes(tenantId, source.records().stream().map(OrderEntity::getId).toList());
|
||||
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId,
|
||||
source.records().stream().map(OrderEntity::getId).toList());
|
||||
@@ -118,7 +117,7 @@ public class UnifiedOrderQueryService {
|
||||
orders.findParties(tenantId, orderId), includeInternalNotes ? orders.findNotes(tenantId, orderId) : List.of(),
|
||||
orders.findHistory(tenantId, orderId));
|
||||
FirstlegUnifiedOrderClient.Detail inbound = order.getStatus() == OrderStatus.SUBMITTED
|
||||
? null : firstlegDetail(tenantId, orderId);
|
||||
? optionalFirstlegDetail(tenantId, orderId) : firstlegDetail(tenantId, orderId);
|
||||
String latestNote = orderDetail.internalNotes().isEmpty() ? null : orderDetail.internalNotes().get(0).getContent();
|
||||
String creatorName = orderDetail.statusHistory().stream()
|
||||
.filter(item -> "CREATE".equals(item.getAction()))
|
||||
@@ -193,6 +192,15 @@ public class UnifiedOrderQueryService {
|
||||
Function.identity(), (left, right) -> left));
|
||||
}
|
||||
|
||||
private Map<Long, FirstlegUnifiedOrderClient.Summary> optionalSummaries(Long tenantId, List<Long> orderIds) {
|
||||
try {
|
||||
return summaries(tenantId, orderIds);
|
||||
} catch (BizException ignored) {
|
||||
// Order 待审核列表仍可独立使用;Firstleg 预入库投影恢复后再开放提前测量入口。
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private FirstlegUnifiedOrderClient.Detail firstlegDetail(long tenantId, long orderId) {
|
||||
FirstlegUnifiedOrderClient.Detail detail = requireNullable(call(() -> firstleg.detail(internalToken,
|
||||
new FirstlegUnifiedOrderClient.DetailRequest(tenantId, orderId))), "统一订单履约详情响应无效");
|
||||
@@ -200,6 +208,15 @@ public class UnifiedOrderQueryService {
|
||||
return detail;
|
||||
}
|
||||
|
||||
|
||||
private FirstlegUnifiedOrderClient.Detail optionalFirstlegDetail(long tenantId, long orderId) {
|
||||
try {
|
||||
return firstlegDetail(tenantId, orderId);
|
||||
} catch (BizException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<FirstlegUnifiedOrderClient.Summary> validateSummaries(
|
||||
List<FirstlegUnifiedOrderClient.Summary> summaries, String message) {
|
||||
return summaries.stream().map(summary -> validateSummary(summary, message)).toList();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.client.WarehouseClient;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
@@ -19,6 +21,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalTime;
|
||||
@@ -272,6 +275,30 @@ class OrderApplicationServiceStateTest {
|
||||
verify(repository).saveOrder(20002L, order);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittingDraftPreparesInboundForWarehouseMeasurementBeforeReview() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderEntity order = persistedDraft("SELF_EXPRESS", "OVERSEAS");
|
||||
when(repository.findOwned(20002L, 1L)).thenReturn(order);
|
||||
when(repository.findCargo(20002L, 1L)).thenReturn(List.of(completeCargo()));
|
||||
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
||||
when(repository.saveOrder(20002L, order)).thenReturn(1);
|
||||
FirstlegInboundClient client = mock(FirstlegInboundClient.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ObjectProvider<FirstlegInboundClient> provider = mock(ObjectProvider.class);
|
||||
when(provider.getIfAvailable()).thenReturn(client);
|
||||
when(client.prepare(eq("internal-token"), any())).thenReturn(ApiResponse.success(99L));
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class),
|
||||
mock(OrderMasterDataGuard.class));
|
||||
ReflectionTestUtils.setField(service, "firstlegProvider", provider);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
|
||||
service.transit(20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null);
|
||||
|
||||
verify(client).prepare(eq("internal-token"), argThat(command -> command.orderId() == 1L
|
||||
&& command.totalPackageCount() == 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeStoresTheExternalInboundCodeOnTheDraft() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
|
||||
@@ -23,6 +23,22 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class OrderInboundOutboxPublisherTest {
|
||||
@Test
|
||||
void matchingPreparedEventIsDeliveredAndPublished() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_PREPARED", 11L, 21L,
|
||||
completePayload(11L, 21L, "BUY_DECLARATION")));
|
||||
when(fixture.client.prepare(eq("internal-token"), any())).thenReturn(ApiResponse.success(99L));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verify(fixture.client).prepare(eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.argThat(command -> command.orderId() == 21L
|
||||
&& command.totalPackageCount() == 4));
|
||||
verify(fixture.client, never()).create(any(), any());
|
||||
verify(fixture.repository).markOutboxPublished(eq(11L), eq(41L), any(LocalDateTime.class));
|
||||
verify(fixture.repository, never()).markOutboxRetry(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchingApprovedEventIsDeliveredAndPublished() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_APPROVED", 11L, 21L, payload(11L, 21L)));
|
||||
|
||||
@@ -81,7 +81,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
assertThat(row.latestInternalNote()).isEqualTo("客服已确认提货时间");
|
||||
assertThat(row.relation()).isNull();
|
||||
});
|
||||
verify(firstleg, never()).summaries(any(), any());
|
||||
verify(firstleg).summaries(eq("internal-token"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -439,7 +439,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingReviewDetailUsesOrderInboundCodeWithoutCallingFirstleg() {
|
||||
void pendingReviewDetailUsesOrderInboundCodeWhenPreparingProjectionIsUnavailable() {
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED);
|
||||
order.setInboundCode("RK-77");
|
||||
when(orders.findOwned(10001L, 77L)).thenReturn(order);
|
||||
@@ -452,7 +452,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
|
||||
assertThat(result.summary().inboundCode()).isEqualTo("RK-77");
|
||||
assertThat(result.order().order().getInboundCode()).isEqualTo("RK-77");
|
||||
verify(firstleg, never()).detail(any(), any());
|
||||
verify(firstleg).detail(eq("internal-token"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user