fix(transfer): settle linked relabel fees after final receipt
This commit is contained in:
+53
-16
@@ -614,22 +614,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
&& !preDispatched) {
|
||||
releaseInventory(customerId, operatorId, application);
|
||||
}
|
||||
if (!"NOT_REQUIRED".equals(application.getWalletStatus())) {
|
||||
BigDecimal quantity = BigDecimal.valueOf(Objects.requireNonNullElse(application.getTotalPackageCount(), 0));
|
||||
feeSources.publishApproved(operatorId, "RELABEL:" + relabelNo,
|
||||
new OverseasFeeSourceDtos.SaveRequest(customerId, linkedOrders.get(0).getCustomerName(),
|
||||
"OVERSEAS_RELABEL_FEE", "RELABEL_ORDER", application.getId(), relabelNo,
|
||||
application.getLastlegOrderId(), null, null, null, null, null,
|
||||
parseLong(application.getOverseasWarehouseId()), "RELABEL_SERVICE", "换标费", "RECEIVABLE",
|
||||
null, null, null, null, null, null, null, List.of(), LocalDate.now(),
|
||||
application.getChargeCurrency(), application.getChargeAmount(),
|
||||
quantity.signum() > 0 ? quantity : null, quantity.signum() > 0 ? "BOX" : null,
|
||||
quantity.signum() > 0 ? application.getChargeAmount().divide(quantity, 6, java.math.RoundingMode.HALF_UP) : null,
|
||||
null, null));
|
||||
walletCommands.publish("SETTLE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null,
|
||||
"换标完成,结算费用", 2L);
|
||||
application.setWalletStatus("SETTLE_PENDING"); applications.updateById(application);
|
||||
}
|
||||
publishCompletionFeesAndSettlement(application, operatorId, linkedOrders);
|
||||
} else if (Set.of("REJECTED", "CANCELLED").contains(next)) {
|
||||
releaseInventory(customerId, operatorId, application);
|
||||
if ("FROZEN".equals(application.getWalletStatus())) {
|
||||
@@ -795,6 +780,58 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
insertHistory(customerId, application.getId(), before, "PROCESSING", operatorId, remark, LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void completeWarehouseTransferFulfillment(String customerId, String relabelNo, Long transferId,
|
||||
String operatorId) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
if (application == null || transfer == null || !customerId.equals(application.getCustomerId())
|
||||
|| !customerId.equals(transfer.getCustomerId()) || !Objects.equals(transfer.getId(), transferId)
|
||||
|| !Boolean.TRUE.equals(transfer.getNeedRelabel()) || !"RECEIVED".equals(transfer.getStatus())
|
||||
|| !Objects.equals(application.getId(), transfer.getRelabelApplicationId())
|
||||
|| !Objects.equals(application.getLastlegOrderId(), transfer.getLastlegOrderId())
|
||||
|| !Objects.equals(application.getCustomerOrderNo(), transfer.getTransferNo())
|
||||
|| application.getCustomerConfirmedAt() == null || transfer.getRelabelCompletedAt() == null) {
|
||||
throw conflict("本票换标完成归属或客户确认不一致");
|
||||
}
|
||||
// Completion replay has no side effects, including for an old TEST row missing settlement.
|
||||
if ("COMPLETED".equals(application.getStatus())) return;
|
||||
if (!READY_FOR_OUTBOUND.equals(application.getStatus())
|
||||
|| !"OUTBOUNDED".equals(application.getFulfillmentStatus())
|
||||
|| !Set.of("FROZEN", "NOT_REQUIRED").contains(application.getWalletStatus())) {
|
||||
throw conflict("本票换标尚未完成出库或资金冻结");
|
||||
}
|
||||
List<LastlegOrder> linkedOrders = tailOrders(application);
|
||||
if (linkedOrders.isEmpty()) throw conflict("Linked tail-leg order is missing");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
application.setStatus("COMPLETED"); application.setFulfillmentStatus("COMPLETED");
|
||||
application.setUpdatedAt(now); applications.updateById(application);
|
||||
// The original transfer already released/transformed/shipped its own stock; never release it again.
|
||||
publishCompletionFeesAndSettlement(application, operatorId, linkedOrders);
|
||||
insertHistory(customerId, application.getId(), READY_FOR_OUTBOUND, "COMPLETED", operatorId,
|
||||
"Warehouse transfer received; relabel fulfillment completed", now);
|
||||
}
|
||||
|
||||
private void publishCompletionFeesAndSettlement(RelabelApplication application, String operatorId,
|
||||
List<LastlegOrder> linkedOrders) {
|
||||
if ("NOT_REQUIRED".equals(application.getWalletStatus())) return;
|
||||
BigDecimal quantity = BigDecimal.valueOf(Objects.requireNonNullElse(application.getTotalPackageCount(), 0));
|
||||
String customerId = application.getCustomerId(), relabelNo = application.getRelabelNo();
|
||||
feeSources.publishApproved(operatorId, "RELABEL:" + relabelNo,
|
||||
new OverseasFeeSourceDtos.SaveRequest(customerId, linkedOrders.get(0).getCustomerName(),
|
||||
"OVERSEAS_RELABEL_FEE", "RELABEL_ORDER", application.getId(), relabelNo,
|
||||
application.getLastlegOrderId(), null, null, null, null, null,
|
||||
parseLong(application.getOverseasWarehouseId()), "RELABEL_SERVICE", "换标费", "RECEIVABLE",
|
||||
null, null, null, null, null, null, null, List.of(), LocalDate.now(),
|
||||
application.getChargeCurrency(), application.getChargeAmount(),
|
||||
quantity.signum() > 0 ? quantity : null, quantity.signum() > 0 ? "BOX" : null,
|
||||
quantity.signum() > 0 ? application.getChargeAmount().divide(quantity, 6, java.math.RoundingMode.HALF_UP) : null,
|
||||
null, null));
|
||||
walletCommands.publish("SETTLE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null,
|
||||
"换标完成,结算费用", 2L);
|
||||
application.setWalletStatus("SETTLE_PENDING"); applications.updateById(application);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail confirmCompletion(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.CustomerConfirm request) {
|
||||
|
||||
+2
-2
@@ -1283,8 +1283,8 @@ public class WarehouseTransferService {
|
||||
RelabelApplication linked = relabelApplications.lockNo(customerId, transfer.getRelabelNo());
|
||||
if (linked == null || !Objects.equals(linked.getId(), transfer.getRelabelApplicationId()))
|
||||
throw conflict("本票换标完成归属不一致");
|
||||
linked.setStatus("COMPLETED"); linked.setFulfillmentStatus("COMPLETED");
|
||||
linked.setUpdatedAt(now); relabelApplications.updateById(linked);
|
||||
relabelService.completeWarehouseTransferFulfillment(customerId, transfer.getRelabelNo(),
|
||||
transfer.getId(), operatorId);
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
+86
@@ -54,7 +54,9 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -809,6 +811,90 @@ class RelabelApplicationServiceTest {
|
||||
return application;
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivedTransferPublishesFrozenQuoteFeeAndSettleOnceWithoutReleasingStockAgain() {
|
||||
RelabelApplication app = transferCompletionFixture();
|
||||
service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1");
|
||||
service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1");
|
||||
ArgumentCaptor<com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest> fee =
|
||||
ArgumentCaptor.forClass(com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest.class);
|
||||
verify(feeSources).publishApproved(eq("operator-1"), eq("RELABEL:FBR-CLOSURE"), fee.capture());
|
||||
assertEquals(new BigDecimal("0.01"), fee.getValue().amount());
|
||||
assertEquals("CNY", fee.getValue().currency()); assertEquals("RELABEL_ORDER", fee.getValue().businessType());
|
||||
assertEquals("FBR-CLOSURE", fee.getValue().businessNo()); assertEquals(BigDecimal.ONE, fee.getValue().chargeableQuantity());
|
||||
verify(walletCommands).publish(eq("SETTLE"), eq(10001L), eq("RELABEL_ORDER"), eq("FBR-CLOSURE"),
|
||||
isNull(), eq("换标完成,结算费用"), eq(2L));
|
||||
assertEquals("COMPLETED", app.getStatus()); assertEquals("COMPLETED", app.getFulfillmentStatus());
|
||||
assertEquals("SETTLE_PENDING", app.getWalletStatus()); assertEquals(new BigDecimal("0.01"), app.getChargeAmount());
|
||||
assertEquals("CNY", app.getChargeCurrency());
|
||||
verify(inventoryCommands, never()).release(any(), any(), any());
|
||||
verify(inventoryCommands, never()).releaseTransferRelabel(any(), any(), any(), any());
|
||||
verify(transferRelabelCompletion, never()).complete(any(), any(), any(), any(), any());
|
||||
verify(histories, times(1)).insert(any(RelabelStatusHistory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivedTransferWithNoRequiredFundsCompletesWithoutWalletOrFeeCommands() {
|
||||
RelabelApplication app = transferCompletionFixture(); app.setWalletStatus("NOT_REQUIRED"); app.setChargeAmount(BigDecimal.ZERO);
|
||||
service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1");
|
||||
assertEquals("COMPLETED", app.getStatus()); assertEquals("NOT_REQUIRED", app.getWalletStatus());
|
||||
verify(feeSources, never()).publishApproved(any(), any(), any());
|
||||
verify(walletCommands, never()).publish(any(), anyLong(), any(), any(), any(), any(), anyLong());
|
||||
verify(inventoryCommands, never()).release(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferCompletionRejectsWrongLinkUnreceivedStockMissingConfirmationOrFailedFreeze() {
|
||||
RelabelApplication app = transferCompletionFixture();
|
||||
assertThrows(ResponseStatusException.class, () -> service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 92L, "operator-1"));
|
||||
app.setCustomerConfirmedAt(null);
|
||||
assertThrows(ResponseStatusException.class, () -> service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1"));
|
||||
app.setCustomerConfirmedAt(LocalDateTime.now()); app.setWalletStatus("FREEZE_FAILED");
|
||||
assertThrows(ResponseStatusException.class, () -> service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1"));
|
||||
app.setWalletStatus("FROZEN");
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockByRelabelNo("10001", app.getRelabelNo());
|
||||
transfer.setStatus("PARTIALLY_RECEIVED");
|
||||
assertThrows(ResponseStatusException.class, () -> service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1"));
|
||||
transfer.setStatus("RECEIVED"); transfer.setRelabelApplicationId(999L);
|
||||
assertThrows(ResponseStatusException.class, () -> service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1"));
|
||||
verify(feeSources, never()).publishApproved(any(), any(), any());
|
||||
verify(walletCommands, never()).publish(any(), anyLong(), any(), any(), any(), any(), anyLong());
|
||||
verify(applications, never()).updateById(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void alreadyCompletedFrozenTestRowIsNotBackfilledByCompletionReplay() {
|
||||
RelabelApplication app = transferCompletionFixture(); app.setStatus("COMPLETED"); app.setFulfillmentStatus("COMPLETED");
|
||||
service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1");
|
||||
assertEquals("FROZEN", app.getWalletStatus());
|
||||
verify(feeSources, never()).publishApproved(any(), any(), any());
|
||||
verify(walletCommands, never()).publish(any(), anyLong(), any(), any(), any(), any(), anyLong());
|
||||
verify(applications, never()).updateById(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void feePublicationFailureNeverQueuesSettlement() {
|
||||
RelabelApplication app = transferCompletionFixture();
|
||||
when(feeSources.publishApproved(any(), any(), any())).thenThrow(new IllegalStateException("fee unavailable"));
|
||||
assertThrows(IllegalStateException.class, () -> service.completeWarehouseTransferFulfillment("10001", app.getRelabelNo(), 91L, "operator-1"));
|
||||
verify(walletCommands, never()).publish(any(), anyLong(), any(), any(), any(), any(), anyLong());
|
||||
}
|
||||
|
||||
private RelabelApplication transferCompletionFixture() {
|
||||
RelabelApplication app = application(11L, "FBR-CLOSURE", "10001", "key", null, RelabelApplicationService.READY_FOR_OUTBOUND);
|
||||
app.setLastlegOrderId(21L); app.setCustomerOrderNo("WTR-CLOSURE"); app.setOverseasWarehouseId("900");
|
||||
app.setCustomerConfirmedAt(LocalDateTime.now()); app.setFulfillmentStatus("OUTBOUNDED");
|
||||
app.setTotalPackageCount(1); app.setNotifiedPackageCount(1); app.setChargeAmount(new BigDecimal("0.01"));
|
||||
WarehouseTransferOrder transfer = new WarehouseTransferOrder(); transfer.setId(91L); transfer.setCustomerId("10001");
|
||||
transfer.setTransferNo("WTR-CLOSURE"); transfer.setLastlegOrderId(21L); transfer.setRelabelApplicationId(11L);
|
||||
transfer.setNeedRelabel(true); transfer.setStatus("RECEIVED"); transfer.setRelabelCompletedAt(LocalDateTime.now());
|
||||
when(applications.lockNo("10001", "FBR-CLOSURE")).thenReturn(app);
|
||||
when(warehouseTransfers.lockByRelabelNo("10001", "FBR-CLOSURE")).thenReturn(transfer);
|
||||
LastlegOrder order = order(21L, "10001", "LMO-CLOSURE", "COMPLETED"); order.setCustomerName("测试客户");
|
||||
lenient().when(orders.selectById(21L)).thenReturn(order);
|
||||
return app;
|
||||
}
|
||||
|
||||
private void stubDetail(RelabelApplication application) {
|
||||
when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application);
|
||||
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6)));
|
||||
|
||||
+10
@@ -74,6 +74,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -853,10 +854,19 @@ class WarehouseTransferServiceTest {
|
||||
verify(inventory,never()).ship(anyString(),anyString(),any());assertEquals("OUTBOUNDED",app.getFulfillmentStatus());
|
||||
var mixed=balance(601,101,"IN-1");mixed.setSkuCode("MIXED");when(inventoryCommands.lockBalance("TENANT-A",601L)).thenReturn(mixed);
|
||||
when(inventory.stockIn(eq("TENANT-A"),eq("USER-1"),any())).thenReturn(new InventoryCommandResponse(1L,"STOCK_IN",801L,901L,5,0,0,0,false,false));
|
||||
doAnswer(call -> { app.setStatus("COMPLETED"); app.setFulfillmentStatus("COMPLETED");
|
||||
app.setWalletStatus("SETTLE_PENDING"); return null; }).when(relabelService)
|
||||
.completeWarehouseTransferFulfillment("TENANT-A", "RL-OWN", 21L, "USER-1");
|
||||
service.receive("TENANT-A","WTR-1","USER-1",receiptRequest());
|
||||
var received=ArgumentCaptor.forClass(com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory).stockIn(eq("TENANT-A"),eq("USER-1"),received.capture());assertEquals("NEW",received.getValue().skuCode());
|
||||
assertEquals("RECEIVED",transfer.getStatus());assertEquals("COMPLETED",app.getStatus());assertEquals("COMPLETED",app.getFulfillmentStatus());
|
||||
var savedReceipt = ArgumentCaptor.forClass(com.baiyue.lastleg.transfer.entity.WarehouseTransferReceipt.class);
|
||||
verify(transferReceipts).insert(savedReceipt.capture());
|
||||
when(transferReceipts.findSource("TENANT-A", 21L, current.getInventoryPositionId())).thenReturn(savedReceipt.getValue());
|
||||
service.receive("TENANT-A","WTR-1","USER-1",receiptRequest());
|
||||
verify(relabelService, times(1)).completeWarehouseTransferFulfillment("TENANT-A", "RL-OWN", 21L, "USER-1");
|
||||
assertEquals("SETTLE_PENDING", app.getWalletStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user