暂存提交
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.service.CargoDiscardInventoryAdjustmentService;
|
||||
import com.baiyue.lastleg.discard.service.CargoDiscardService;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest(classes = CargoDiscardMysqlAcceptanceTest.AcceptanceApplication.class, properties = {
|
||||
"spring.sql.init.mode=never",
|
||||
"spring.cloud.nacos.discovery.enabled=false",
|
||||
"spring.cloud.nacos.discovery.register-enabled=false"
|
||||
})
|
||||
@EnabledIfSystemProperty(named = "cargo.discard.acceptance", matches = "true")
|
||||
class CargoDiscardMysqlAcceptanceTest {
|
||||
private static final String CUSTOMER = "990001";
|
||||
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
@Autowired private CargoDiscardService service;
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Import({CargoDiscardService.class, CargoDiscardInventoryAdjustmentService.class})
|
||||
@MapperScan({
|
||||
"com.baiyue.lastleg.mapper",
|
||||
"com.baiyue.lastleg.discard.mapper",
|
||||
"com.baiyue.lastleg.dropship.mapper"
|
||||
})
|
||||
static class AcceptanceApplication {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createApprovalRejectionPendingAndInsufficientStockScenarios() {
|
||||
resetAcceptanceTenant();
|
||||
seedOrder(990001001L, "RK-ACCEPT-APPROVE", "JF-ACCEPT-001", 20);
|
||||
seedProfile(990001101L, 990001001L, "PALLET-A");
|
||||
seedSku(990001201L, 990001001L, 990001101L, "SKU-BOX-RED", "红色折叠收纳箱", 12);
|
||||
seedSku(990001202L, 990001001L, 990001101L, "SKU-RACK-KITCHEN", "厨房置物架", 8);
|
||||
seedCartons(990001001L, 990001101L, 990001301L, "PALLET-A", "SKU-BOX-RED", "RED", 12);
|
||||
seedCartons(990001001L, 990001101L, 990001401L, "PALLET-A", "SKU-RACK-KITCHEN", "RACK", 8);
|
||||
|
||||
seedOrder(990001002L, "RK-ACCEPT-REJECT", "JF-ACCEPT-002", 10);
|
||||
seedProfile(990001102L, 990001002L, "PALLET-B");
|
||||
seedSku(990001203L, 990001002L, 990001102L, "SKU-LAMP-WHITE", "白色台灯", 10);
|
||||
seedCartons(990001002L, 990001102L, 990001501L, "PALLET-B", "SKU-LAMP-WHITE", "LAMP", 10);
|
||||
|
||||
seedOrder(990001003L, "RK-ACCEPT-PENDING", "JF-ACCEPT-003", 7);
|
||||
seedProfile(990001103L, 990001003L, "PALLET-C");
|
||||
seedSku(990001204L, 990001003L, 990001103L, "SKU-MAT-BLUE", "蓝色瑜伽垫", 7);
|
||||
seedCartons(990001003L, 990001103L, 990001601L, "PALLET-C", "SKU-MAT-BLUE", "MAT", 7);
|
||||
|
||||
var approveRequest = new CargoDiscardRequests.Create(List.of(
|
||||
new CargoDiscardRequests.Item(990001001L, "SKU-BOX-RED", 3),
|
||||
new CargoDiscardRequests.Item(990001001L, "SKU-RACK-KITCHEN", 2)),
|
||||
"ACCEPT-DISCARD-APPROVE", "验收:破损货物,同意废弃");
|
||||
var approved = service.create(CUSTOMER, "acceptance-creator", approveRequest);
|
||||
assertEquals(20L, skuPackages(990001001L));
|
||||
approved = service.review(CUSTOMER, approved.summary().discardNo(), "acceptance-reviewer",
|
||||
new CargoDiscardRequests.Review("APPROVE", "仓库复核破损属实"));
|
||||
assertEquals(CargoDiscardService.APPROVED, approved.summary().status());
|
||||
assertEquals(15L, skuPackages(990001001L));
|
||||
|
||||
var rejected = service.create(CUSTOMER, "acceptance-creator", new CargoDiscardRequests.Create(
|
||||
List.of(new CargoDiscardRequests.Item(990001002L, "SKU-LAMP-WHITE", 4)),
|
||||
"ACCEPT-DISCARD-REJECT", "验收:外箱可修复"));
|
||||
assertEquals(10L, skuPackages(990001002L));
|
||||
rejected = service.review(CUSTOMER, rejected.summary().discardNo(), "acceptance-reviewer",
|
||||
new CargoDiscardRequests.Review("REJECT", "复核后无需废弃"));
|
||||
assertEquals(CargoDiscardService.REJECTED, rejected.summary().status());
|
||||
assertEquals(10L, skuPackages(990001002L));
|
||||
|
||||
var pending = service.create(CUSTOMER, "acceptance-creator", new CargoDiscardRequests.Create(
|
||||
List.of(new CargoDiscardRequests.Item(990001003L, "SKU-MAT-BLUE", 2)),
|
||||
"ACCEPT-DISCARD-PENDING", "验收:等待仓库审核"));
|
||||
assertEquals(CargoDiscardService.PENDING_REVIEW, pending.summary().status());
|
||||
assertEquals(7L, skuPackages(990001003L));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.create(CUSTOMER, "acceptance-creator",
|
||||
new CargoDiscardRequests.Create(List.of(
|
||||
new CargoDiscardRequests.Item(990001003L, "SKU-MAT-BLUE", 99)),
|
||||
"ACCEPT-DISCARD-INSUFFICIENT", "验收:库存不足应失败")));
|
||||
|
||||
assertEquals(1, count("SELECT COUNT(*) FROM ll_cargo_discard_application WHERE customer_id=? AND status='APPROVED'"));
|
||||
assertEquals(1, count("SELECT COUNT(*) FROM ll_cargo_discard_application WHERE customer_id=? AND status='REJECTED'"));
|
||||
assertEquals(1, count("SELECT COUNT(*) FROM ll_cargo_discard_application WHERE customer_id=? AND status='PENDING_REVIEW'"));
|
||||
assertEquals(5, count("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=? AND order_id=990001001 AND status='DISCARDED'"));
|
||||
assertEquals(0, count("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=? AND order_id=990001002 AND status='DISCARD_PENDING'"));
|
||||
assertEquals(2, count("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=? AND order_id=990001003 AND status='DISCARD_PENDING'"));
|
||||
assertEquals(5, count("SELECT COUNT(*) FROM ll_dropship_carton_ledger WHERE customer_id=? AND operation_type='DISCARD'"));
|
||||
assertEquals(5, count("SELECT COUNT(DISTINCT idempotency_key) FROM ll_dropship_carton_ledger WHERE customer_id=? AND operation_type='DISCARD'"));
|
||||
String audit = jdbc.queryForObject("SELECT remark FROM ll_cargo_discard_status_history WHERE customer_id=? " +
|
||||
"AND to_status='APPROVED'", String.class, CUSTOMER);
|
||||
assertTrue(audit.contains("inventoryBefore=20, discarded=5, inventoryAfter=15"));
|
||||
}
|
||||
|
||||
private void resetAcceptanceTenant() {
|
||||
jdbc.update("DELETE FROM ll_dropship_carton_ledger WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_cargo_discard_status_history WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_cargo_discard_carton WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_cargo_discard_item WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_cargo_discard_application WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_dropship_carton WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_dropship_sku_inventory WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_dropship_order_profile WHERE customer_id=?", CUSTOMER);
|
||||
jdbc.update("DELETE FROM ll_order WHERE customer_id=?", CUSTOMER);
|
||||
}
|
||||
|
||||
private void seedOrder(long id, String inboundCode, String orderNo, int packages) {
|
||||
jdbc.update("INSERT INTO ll_order(id,order_no,customer_id,order_type,inbound_code,source_region," +
|
||||
"source_inbound_id,customer_name,overseas_warehouse_id,overseas_warehouse_name,pallet_count," +
|
||||
"package_count,product_quantity,current_status,source_event_id,source_loading_plan_id,source_payload_hash," +
|
||||
"inbound_order_no,created_at,updated_at) VALUES(?,?,?,'ONE_CLICK_DROPSHIP',?,'ACCEPTANCE',?," +
|
||||
"'废弃验收客户','WH-US-LAX','美西洛杉矶仓',1,?,?, 'WAREHOUSED',?,?,?, ?,NOW(3),NOW(3))",
|
||||
id, orderNo, CUSTOMER, inboundCode, "ACCEPT-" + id, packages, packages * 10,
|
||||
"ACCEPT-EVENT-" + id, "ACCEPT-PLAN-" + id, "sha256:acceptance-" + id, orderNo);
|
||||
}
|
||||
|
||||
private void seedProfile(long id, long orderId, String palletNo) {
|
||||
jdbc.update("INSERT INTO ll_dropship_order_profile(id,customer_id,order_id,warehouse_pallet_id,pallet_no," +
|
||||
"source_pallet_no,status,idempotency_key,version,created_at,updated_at) " +
|
||||
"VALUES(?,?,?, ?,?,?,'RECEIVED',?,0,NOW(3),NOW(3))",
|
||||
id, CUSTOMER, orderId, id, palletNo, "SRC-" + palletNo, "ACCEPT-PROFILE-" + id);
|
||||
}
|
||||
|
||||
private void seedSku(long id, long orderId, long profileId, String sku, String name, int packages) {
|
||||
jdbc.update("INSERT INTO ll_dropship_sku_inventory(id,customer_id,order_id,profile_id,sku_code,product_name," +
|
||||
"package_count,product_quantity,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,NOW(3),NOW(3))",
|
||||
id, CUSTOMER, orderId, profileId, sku, name, packages, packages * 10);
|
||||
}
|
||||
|
||||
private void seedCartons(long orderId, long profileId, long firstId, String palletNo,
|
||||
String sku, String markPrefix, int packages) {
|
||||
for (int index = 1; index <= packages; index++) {
|
||||
jdbc.update("INSERT INTO ll_dropship_carton(id,customer_id,order_id,profile_id,warehouse_pallet_id," +
|
||||
"pallet_no,sku_code,carton_mark,carton_sequence,status,version,created_at,updated_at) " +
|
||||
"VALUES(?,?,?,?,?,?,?,?,?,'AVAILABLE',0,NOW(3),NOW(3))",
|
||||
firstId + index - 1, CUSTOMER, orderId, profileId, profileId, palletNo, sku,
|
||||
"ACCEPT-" + markPrefix + "-" + String.format("%03d", index), index);
|
||||
}
|
||||
}
|
||||
|
||||
private long skuPackages(long orderId) {
|
||||
return jdbc.queryForObject("SELECT SUM(package_count) FROM ll_dropship_sku_inventory WHERE customer_id=? AND order_id=?",
|
||||
Long.class, CUSTOMER, orderId);
|
||||
}
|
||||
|
||||
private int count(String sql) {
|
||||
return jdbc.queryForObject(sql, Integer.class, CUSTOMER);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CargoDiscardSchemaContractTest {
|
||||
@Test
|
||||
void baselineContainsDiscardApplicationReservationAndHistoryTables() throws Exception {
|
||||
String sql;
|
||||
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
|
||||
sql = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_cargo_discard_application"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_cargo_discard_item"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_cargo_discard_carton"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_cargo_discard_status_history"));
|
||||
assertTrue(sql.contains("UNIQUE KEY uk_ll_cargo_discard_carton(application_id,carton_id)"));
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.baiyue.lastleg.discard.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.service.CargoDiscardService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CargoDiscardControllerTest {
|
||||
@Mock CargoDiscardService service;
|
||||
private CargoDiscardController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new CargoDiscardController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reviewRequiresExplicitApprovalPermission() {
|
||||
AuthenticatedUser user = user(Set.of());
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> controller.review(
|
||||
user, null, "FH-1", new CargoDiscardRequests.Review("APPROVE", null)));
|
||||
assertEquals(403, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantTenantComesFromTrustedIdentityWhenApproving() {
|
||||
AuthenticatedUser user = user(Set.of(CargoDiscardService.APPROVAL_PERMISSION));
|
||||
CargoDiscardRequests.Review request = new CargoDiscardRequests.Review("APPROVE", "ok");
|
||||
controller.review(user, "999", "FH-1", request);
|
||||
verify(service).review("100", "FH-1", "7", request);
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, 100L, 100L, 8L, "reviewer", "MERCHANT", "WEB", permissions);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
@Mock CargoDiscardInventoryMapper inventory;
|
||||
@Mock DropshipCartonLedgerMapper ledgers;
|
||||
private CargoDiscardInventoryAdjustmentService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CargoDiscardInventoryAdjustmentService();
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "cartonLedgers", ledgers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void discardRequiresExistingTransactionAndConservesPackages() throws Exception {
|
||||
Transactional transactional = CargoDiscardInventoryAdjustmentService.class
|
||||
.getMethod("discard", String.class, CargoDiscardApplication.class, List.class, Map.class,
|
||||
String.class, LocalDateTime.class).getAnnotation(Transactional.class);
|
||||
assertEquals(Propagation.MANDATORY, transactional.propagation());
|
||||
CargoDiscardApplication application = application(2);
|
||||
CargoDiscardItem item = item(2);
|
||||
when(inventory.lockSkuInventory("100", 301L)).thenReturn(sku(5));
|
||||
when(inventory.deductSkuInventory("100", 301L, 2)).thenReturn(1);
|
||||
when(inventory.skuPackages("100", 301L)).thenReturn(3L);
|
||||
when(inventory.lockCarton(eq("100"), anyLong())).thenAnswer(invocation -> carton(invocation.getArgument(1)));
|
||||
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("DISCARD_PENDING"), eq("DISCARDED"))).thenReturn(1);
|
||||
|
||||
var result = service.discard("100", application, List.of(item),
|
||||
Map.of(item.getId(), List.of(link(1L), link(2L))), "9", LocalDateTime.now());
|
||||
|
||||
assertEquals(5, result.beforePackages());
|
||||
assertEquals(2, result.discardedPackages());
|
||||
assertEquals(3, result.afterPackages());
|
||||
ArgumentCaptor<DropshipCartonLedger> facts = ArgumentCaptor.forClass(DropshipCartonLedger.class);
|
||||
verify(ledgers, times(2)).insert(facts.capture());
|
||||
assertTrue(facts.getAllValues().stream().allMatch(value -> "DISCARD".equals(value.getOperationType())
|
||||
&& value.getIdempotencyKey().startsWith("CARGO_DISCARD:401:")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void discardRejectsAnyPackageConservationMismatch() {
|
||||
CargoDiscardApplication application = application(2);
|
||||
CargoDiscardItem item = item(2);
|
||||
when(inventory.lockSkuInventory("100", 301L)).thenReturn(sku(5));
|
||||
when(inventory.deductSkuInventory("100", 301L, 2)).thenReturn(1);
|
||||
when(inventory.skuPackages("100", 301L)).thenReturn(4L);
|
||||
when(inventory.lockCarton(eq("100"), anyLong())).thenAnswer(invocation -> carton(invocation.getArgument(1)));
|
||||
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("DISCARD_PENDING"), eq("DISCARDED"))).thenReturn(1);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.discard("100", application, List.of(item),
|
||||
Map.of(item.getId(), List.of(link(1L), link(2L))), "9", LocalDateTime.now()));
|
||||
}
|
||||
|
||||
private CargoDiscardApplication application(int packages) {
|
||||
CargoDiscardApplication value = new CargoDiscardApplication();
|
||||
value.setId(401L); value.setDiscardNo("FH-1"); value.setTotalPackages(packages); value.setRemark("damaged");
|
||||
return value;
|
||||
}
|
||||
private CargoDiscardItem item(int packages) {
|
||||
CargoDiscardItem value = new CargoDiscardItem();
|
||||
value.setId(501L); value.setOrderId(101L); value.setSkuInventoryId(301L); value.setSkuCode("SKU-1");
|
||||
value.setDiscardPackages(packages); return value;
|
||||
}
|
||||
private DropshipSkuInventory sku(long packages) {
|
||||
DropshipSkuInventory value = new DropshipSkuInventory(); value.setId(301L); value.setPackageCount(packages); return value;
|
||||
}
|
||||
private CargoDiscardCarton link(long id) {
|
||||
CargoDiscardCarton value = new CargoDiscardCarton(); value.setItemId(501L); value.setCartonId(id); return value;
|
||||
}
|
||||
private DropshipCarton carton(long id) {
|
||||
DropshipCarton value = new DropshipCarton(); value.setId(id); value.setCartonMark("C-" + id); return value;
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardStatusHistory;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardApplicationMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardCartonMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CargoDiscardServiceTest {
|
||||
@Mock CargoDiscardApplicationMapper applications;
|
||||
@Mock CargoDiscardItemMapper items;
|
||||
@Mock CargoDiscardCartonMapper discardCartons;
|
||||
@Mock CargoDiscardStatusHistoryMapper histories;
|
||||
@Mock CargoDiscardInventoryMapper inventory;
|
||||
@Mock CargoDiscardInventoryAdjustmentService adjustments;
|
||||
@Mock LastlegOrderMapper orders;
|
||||
|
||||
private CargoDiscardService service;
|
||||
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
|
||||
private final List<CargoDiscardItem> storedItems = new ArrayList<>();
|
||||
private final List<CargoDiscardCarton> storedLinks = new ArrayList<>();
|
||||
private final List<CargoDiscardStatusHistory> storedHistories = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CargoDiscardService();
|
||||
ReflectionTestUtils.setField(service, "applications", applications);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "discardCartons", discardCartons);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "adjustments", adjustments);
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
|
||||
lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; });
|
||||
lenient().when(items.insert(any(CargoDiscardItem.class))).thenAnswer(invocation -> { storedItems.add(invocation.getArgument(0)); return 1; });
|
||||
lenient().when(discardCartons.insert(any(CargoDiscardCarton.class))).thenAnswer(invocation -> { storedLinks.add(invocation.getArgument(0)); return 1; });
|
||||
lenient().when(histories.insert(any(CargoDiscardStatusHistory.class))).thenAnswer(invocation -> { storedHistories.add(invocation.getArgument(0)); return 1; });
|
||||
lenient().when(applications.findNo(any(), any())).thenAnswer(invocation -> storedApplication.get());
|
||||
lenient().when(items.listApplication(any(), anyLong())).thenAnswer(invocation -> List.copyOf(storedItems));
|
||||
lenient().when(discardCartons.listApplication(any(), anyLong())).thenAnswer(invocation -> List.copyOf(storedLinks));
|
||||
lenient().when(histories.listApplication(any(), anyLong())).thenAnswer(invocation -> List.copyOf(storedHistories));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createReservesExactSkuCartonsWithoutDeductingInventory() {
|
||||
LastlegOrder order = order();
|
||||
DropshipSkuInventory sku = skuInventory();
|
||||
when(orders.selectById(101L)).thenReturn(order);
|
||||
when(inventory.countAvailable("100", 101L, "SKU-1")).thenReturn(3L);
|
||||
when(inventory.lockAvailable("100", 101L, "SKU-1", 2)).thenReturn(List.of(carton(1L), carton(2L)));
|
||||
when(inventory.lockSkuInventoryDimension("100", 101L, 201L, "SKU-1")).thenReturn(sku);
|
||||
when(inventory.countAvailableProfile("100", 101L, 201L, "SKU-1")).thenReturn(3L);
|
||||
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("AVAILABLE"), eq("DISCARD_PENDING"))).thenReturn(1);
|
||||
|
||||
var detail = service.create("100", "7", new CargoDiscardRequests.Create(
|
||||
List.of(new CargoDiscardRequests.Item(101L, "SKU-1", 2)), "discard-create-1", "damaged"));
|
||||
|
||||
assertEquals(CargoDiscardService.PENDING_REVIEW, detail.summary().status());
|
||||
assertEquals(2, detail.summary().totalPackages());
|
||||
assertEquals(new BigDecimal("10.00"), detail.summary().totalFee());
|
||||
assertEquals(2, storedLinks.size());
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalDeductsSkuInventoryAndWritesOneLedgerPerCarton() {
|
||||
CargoDiscardApplication application = pendingApplication();
|
||||
storedApplication.set(application);
|
||||
CargoDiscardItem item = item();
|
||||
storedItems.add(item);
|
||||
storedLinks.add(link(item.getId(), 1L));
|
||||
storedLinks.add(link(item.getId(), 2L));
|
||||
when(applications.lockNo("100", "FH-1")).thenReturn(application);
|
||||
when(adjustments.discard(eq("100"), eq(application), any(), any(), eq("9"), any()))
|
||||
.thenReturn(new CargoDiscardInventoryAdjustmentService.AdjustmentResult(3, 2, 1));
|
||||
|
||||
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("APPROVE", "ok"));
|
||||
|
||||
assertEquals(CargoDiscardService.APPROVED, detail.summary().status());
|
||||
verify(adjustments).discard(eq("100"), eq(application), any(), any(), eq("9"), any());
|
||||
assertEquals("ok; inventoryBefore=3, discarded=2, inventoryAfter=1",
|
||||
storedHistories.get(storedHistories.size() - 1).getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectionReleasesReservedCartonsWithoutDeduction() {
|
||||
CargoDiscardApplication application = pendingApplication();
|
||||
storedApplication.set(application);
|
||||
CargoDiscardItem item = item();
|
||||
storedItems.add(item);
|
||||
storedLinks.add(link(item.getId(), 1L));
|
||||
when(applications.lockNo("100", "FH-1")).thenReturn(application);
|
||||
when(inventory.changeCartonStatus("100", 1L, "DISCARD_PENDING", "AVAILABLE")).thenReturn(1);
|
||||
|
||||
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("REJECT", "not damaged"));
|
||||
|
||||
assertEquals(CargoDiscardService.REJECTED, detail.summary().status());
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
verify(adjustments, never()).discard(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
private LastlegOrder order() {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(101L); order.setCustomerId("100"); order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
order.setInboundCode("RK-1"); order.setOverseasWarehouseId("WH-1"); order.setOverseasWarehouseName("LA");
|
||||
return order;
|
||||
}
|
||||
|
||||
private DropshipSkuInventory skuInventory() {
|
||||
DropshipSkuInventory value = new DropshipSkuInventory();
|
||||
value.setId(301L); value.setCustomerId("100"); value.setOrderId(101L); value.setProfileId(201L);
|
||||
value.setSkuCode("SKU-1"); value.setProductName("Box"); value.setPackageCount(3L);
|
||||
return value;
|
||||
}
|
||||
|
||||
private DropshipCarton carton(Long id) {
|
||||
DropshipCarton value = new DropshipCarton();
|
||||
value.setId(id); value.setCustomerId("100"); value.setOrderId(101L); value.setProfileId(201L);
|
||||
value.setSkuCode("SKU-1"); value.setCartonMark("C-" + id); value.setStatus("DISCARD_PENDING");
|
||||
return value;
|
||||
}
|
||||
|
||||
private CargoDiscardApplication pendingApplication() {
|
||||
CargoDiscardApplication value = new CargoDiscardApplication();
|
||||
value.setId(401L); value.setCustomerId("100"); value.setDiscardNo("FH-1"); value.setInboundCodes("RK-1");
|
||||
value.setOverseasWarehouseId("WH-1"); value.setTotalPackages(2); value.setFeePerPackage(new BigDecimal("5.00"));
|
||||
value.setTotalFee(new BigDecimal("10.00")); value.setCurrency("USD"); value.setStatus(CargoDiscardService.PENDING_REVIEW);
|
||||
return value;
|
||||
}
|
||||
|
||||
private CargoDiscardItem item() {
|
||||
CargoDiscardItem value = new CargoDiscardItem();
|
||||
value.setId(501L); value.setApplicationId(401L); value.setCustomerId("100"); value.setOrderId(101L);
|
||||
value.setInboundCode("RK-1"); value.setProfileId(201L); value.setSkuInventoryId(301L);
|
||||
value.setSkuCode("SKU-1"); value.setAvailablePackagesSnapshot(3); value.setDiscardPackages(2);
|
||||
return value;
|
||||
}
|
||||
|
||||
private CargoDiscardCarton link(Long itemId, Long cartonId) {
|
||||
CargoDiscardCarton value = new CargoDiscardCarton();
|
||||
value.setId(600L + cartonId); value.setApplicationId(401L); value.setItemId(itemId);
|
||||
value.setCustomerId("100"); value.setCartonId(cartonId); value.setCartonMark("C-" + cartonId);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user