feat: 完成撤销收益扣回與限額改動
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.starry.admin.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -210,12 +211,12 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
void listByPage_keywordFiltersByOrderNoOrClerkName() throws Exception {
|
||||
ensureTenantContext();
|
||||
String marker = ("KW" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
LocalDateTime reference = LocalDateTime.now().plusHours(2);
|
||||
|
||||
PlayOrderInfoEntity orderByNo = persistOrder(marker, "ord", reference, order -> {
|
||||
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
PlayOrderInfoEntity orderByClerk = persistOrder(marker, "clk", reference.minusMinutes(10), order -> {
|
||||
PlayOrderInfoEntity orderByClerk = persistOrder(marker, "clk", reference.plusMinutes(10), order -> {
|
||||
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
|
||||
@@ -225,7 +226,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
|
||||
ObjectNode clerkKeywordPayload = baseQuery();
|
||||
clerkKeywordPayload.put("keyword", "小测官");
|
||||
clerkKeywordPayload.set("purchaserTime", range(reference.minusMinutes(5), reference.plusMinutes(5)));
|
||||
clerkKeywordPayload.set("purchaserTime", range(reference.plusMinutes(5), reference.plusMinutes(15)));
|
||||
clerkKeywordPayload.put("placeType", "1");
|
||||
RecordsResponse clerkResponse = executeList(clerkKeywordPayload);
|
||||
JsonNode clerkRecords = clerkResponse.records;
|
||||
@@ -239,7 +240,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
void listByPage_keywordRespectsAdditionalFilters() throws Exception {
|
||||
ensureTenantContext();
|
||||
String marker = ("KWFLT" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(3);
|
||||
LocalDateTime reference = LocalDateTime.now().plusHours(3);
|
||||
|
||||
PlayOrderInfoEntity assignedOrder = persistOrder(marker, "assigned", reference, order -> {
|
||||
order.setOrderStatus("3");
|
||||
@@ -270,119 +271,183 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("80.00"), "withdrawn");
|
||||
seedEarningLine(order.getId(), new BigDecimal("80.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", BigDecimal.ZERO);
|
||||
payload.put("refundReason", "API撤销-保留收益");
|
||||
payload.put("earningsStrategy", "NONE");
|
||||
payload.put("deductClerkEarnings", false);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
MvcResult response = mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("操作成功"));
|
||||
.andReturn();
|
||||
|
||||
JsonNode reverseRoot = objectMapper.readTree(response.getResponse().getContentAsString());
|
||||
assertThat(reverseRoot.path("code").asInt())
|
||||
.as("response=%s", reverseRoot.toString())
|
||||
.isEqualTo(200);
|
||||
assertThat(reverseRoot.path("success").asBoolean()).isTrue();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_reverseClerkBlockedWhenLocked() throws Exception {
|
||||
void revokeCompletedOrder_reverseClerkCreatesNegativeLineEvenWhenLocked() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "reverse", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("210.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
seedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("20.00"));
|
||||
payload.put("refundReason", "API撤销-冲销收益");
|
||||
payload.put("earningsStrategy", "REVERSE_CLERK");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("20.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
MvcResult response = mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("收益已提现或处理中,无法撤销"));
|
||||
}
|
||||
.andReturn();
|
||||
|
||||
JsonNode reverseRoot = objectMapper.readTree(response.getResponse().getContentAsString());
|
||||
assertThat(reverseRoot.path("code").asInt())
|
||||
.as("response=%s", reverseRoot.toString())
|
||||
.isEqualTo(200);
|
||||
assertThat(reverseRoot.path("success").asBoolean()).isTrue();
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_counterStrategyCreatesNegativeLineAfterWithdrawal() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(90);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "counter", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("230.00"));
|
||||
entity.setEstimatedRevenue(new BigDecimal("150.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("140.00"), "withdrawn");
|
||||
|
||||
String counterClerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID + "-pei-hold";
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("50.00"));
|
||||
payload.put("refundReason", "API撤销-转待扣");
|
||||
payload.put("earningsStrategy", "COUNTER_TO_PEIPEI");
|
||||
payload.put("counterClerkId", counterClerkId);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("操作成功"));
|
||||
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
assertThat(lines).hasSize(2);
|
||||
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
EarningsLineEntity negativeLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-50.00"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("available");
|
||||
assertThat(counterLine.getClerkId()).isEqualTo(counterClerkId);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
assertThat(negativeLine.getAmount()).isEqualByComparingTo(new BigDecimal("-20.00"));
|
||||
assertThat(negativeLine.getClerkId()).isEqualTo(order.getAcceptBy());
|
||||
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_refundAndCounterCreatesRecords() throws Exception {
|
||||
void revokeCompletedOrder_deductKeepsFrozenUnlockSchedule() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(45);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "refundCounter", reference, entity -> {
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(1);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "frozen", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("320.00"));
|
||||
entity.setEstimatedRevenue(new BigDecimal("180.00"));
|
||||
entity.setPaymentSource(OrderConstant.PaymentSource.WX_PAY.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("110.00"), "withdrawn");
|
||||
|
||||
String counterClerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID + "-refund-counter";
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
LocalDateTime unlockAt = LocalDateTime.now().plusHours(3).withNano(0);
|
||||
seedEarningLine(order.getId(), new BigDecimal("90.00"), "frozen", unlockAt);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", true);
|
||||
payload.put("refundAmount", new BigDecimal("60.00"));
|
||||
payload.put("refundReason", "API撤销-退款并待扣");
|
||||
payload.put("earningsStrategy", "COUNTER_TO_PEIPEI");
|
||||
payload.put("counterClerkId", counterClerkId);
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", BigDecimal.ZERO);
|
||||
payload.put("refundReason", "API撤销-冻结扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("30.00"));
|
||||
|
||||
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
EarningsLineEntity negativeLine = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list()
|
||||
.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(negativeLine.getStatus()).isEqualTo("frozen");
|
||||
assertThat(negativeLine.getUnlockTime()).isEqualTo(unlockAt);
|
||||
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductMakesWithdrawnLineAvailable() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "withdrawn", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("188.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
seedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("0.00"));
|
||||
payload.put("refundReason", "API撤销-提现扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("40.00"));
|
||||
|
||||
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
EarningsLineEntity negativeLine = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list()
|
||||
.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(negativeLine.getStatus()).isEqualTo("available");
|
||||
assertThat(negativeLine.getUnlockTime()).isAfter(beforeCall);
|
||||
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_defaultsDeductAmountWhenMissing() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(50);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "autoDeduct", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("260.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("75.00"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-自动扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -390,12 +455,55 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("操作成功"));
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-75.00"));
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_refundAndDeductCreatesRecords() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(35);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "refundDeduct", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("300.00"));
|
||||
entity.setPaymentSource(OrderConstant.PaymentSource.WX_PAY.getCode());
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("120.00"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", true);
|
||||
payload.put("refundAmount", new BigDecimal("80.00"));
|
||||
payload.put("refundReason", "API撤销-退款扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
PlayOrderRefundInfoEntity refundInfo = orderRefundInfoService.selectPlayOrderRefundInfoByOrderId(order.getId());
|
||||
assertThat(refundInfo).isNotNull();
|
||||
assertThat(refundInfo.getRefundAmount()).isEqualByComparingTo(new BigDecimal("60.00"));
|
||||
assertThat(refundInfo.getRefundAmount()).isEqualByComparingTo(new BigDecimal("80.00"));
|
||||
refundIdsToCleanup.add(refundInfo.getId());
|
||||
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
@@ -406,10 +514,155 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-60.00"));
|
||||
assertThat(counterLine.getClerkId()).isEqualTo(counterClerkId);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductLineRespectsFutureUnlockSchedule() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(3).withNano(0);
|
||||
LocalDateTime unlockAt = LocalDateTime.now().plusHours(12).withNano(0);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "futureUnlock", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("220.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("120.00"), "frozen", unlockAt);
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-锁定排期");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("frozen");
|
||||
assertThat(counterLine.getUnlockTime()).isEqualTo(unlockAt);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductLineUnlocksImmediatelyWhenAlreadyAvailable() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(5).withNano(0);
|
||||
LocalDateTime unlockAt = LocalDateTime.now().minusHours(1).withNano(0);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "pastUnlock", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("180.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("90.00"), "available", unlockAt);
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-立即扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("45.00"));
|
||||
|
||||
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("available");
|
||||
assertThat(counterLine.getUnlockTime()).isAfter(beforeCall);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductFailsWhenNoEarningLineExists() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(4);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "noLine", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("150.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-无收益扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("30.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("本单店员收益已全部扣回"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_rejectsDeductAmountBeyondAvailable() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(30);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "overDeduct", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("40.00"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-超额扣");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("扣回金额不能超过本单收益40.00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_blocksNonNormalOrders() throws Exception {
|
||||
ensureTenantContext();
|
||||
@@ -424,7 +677,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
payload.put("orderId", giftOrder.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "gift revoke");
|
||||
payload.put("earningsStrategy", "NONE");
|
||||
payload.put("deductClerkEarnings", false);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -436,6 +689,30 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
.andExpect(jsonPath("$.message").value("仅支持撤销普通服务订单"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRevocationLimits_returnsRemainingValues() throws Exception {
|
||||
ensureTenantContext();
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "limits", LocalDateTime.now().minusHours(3), entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("188.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("45.50"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
mockMvc.perform(get("/order/order/" + order.getId() + "/revocationLimits")
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.maxRefundAmount").value(188.00))
|
||||
.andExpect(jsonPath("$.data.maxDeductAmount").value(45.50))
|
||||
.andExpect(jsonPath("$.data.defaultDeductAmount").value(45.50))
|
||||
.andExpect(jsonPath("$.data.deductible").value(true));
|
||||
}
|
||||
|
||||
private PlayOrderInfoEntity persistOrder(
|
||||
String marker,
|
||||
String token,
|
||||
@@ -525,7 +802,11 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
return array;
|
||||
}
|
||||
|
||||
private String seedLockedEarningLine(String orderId, BigDecimal amount, String status) {
|
||||
private String seedEarningLine(String orderId, BigDecimal amount, String status) {
|
||||
return seedEarningLine(orderId, amount, status, LocalDateTime.now().minusHours(2).withNano(0));
|
||||
}
|
||||
|
||||
private String seedEarningLine(String orderId, BigDecimal amount, String status, LocalDateTime unlockAt) {
|
||||
EarningsLineEntity entity = new EarningsLineEntity();
|
||||
String id = "earn-revoke-" + IdUtils.getUuid();
|
||||
entity.setId(id);
|
||||
@@ -535,14 +816,17 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
entity.setAmount(amount);
|
||||
entity.setStatus(status);
|
||||
entity.setEarningType(EarningsType.ORDER);
|
||||
entity.setUnlockTime(LocalDateTime.now().minusHours(2));
|
||||
entity.setWithdrawalId("withdraw-" + IdUtils.getUuid());
|
||||
entity.setUnlockTime(unlockAt);
|
||||
if ("withdrawn".equals(status) || "withdrawing".equals(status)) {
|
||||
entity.setWithdrawalId("withdraw-" + IdUtils.getUuid());
|
||||
}
|
||||
Date nowDate = toDate(LocalDateTime.now());
|
||||
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setCreatedTime(nowDate);
|
||||
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setUpdatedTime(nowDate);
|
||||
entity.setDeleted(false);
|
||||
ensureTenantContext();
|
||||
earningsService.save(entity);
|
||||
earningsLineIdsToCleanup.add(id);
|
||||
return id;
|
||||
|
||||
Reference in New Issue
Block a user