fix(deduction): make apply idempotent
Some checks failed
Build and Push Backend / docker (push) Has been cancelled

This commit is contained in:
irving
2026-01-19 00:19:02 -05:00
parent fffc623ab0
commit 9b9b1024c8
2 changed files with 135 additions and 1 deletions

View File

@@ -228,6 +228,10 @@ public class EarningsDeductionBatchServiceImpl
if (item == null || !StringUtils.hasText(item.getClerkId())) { if (item == null || !StringUtils.hasText(item.getClerkId())) {
continue; continue;
} }
if (StringUtils.hasText(item.getAdjustmentId())) {
adjustmentService.triggerApplyAsync(item.getAdjustmentId());
continue;
}
BigDecimal amount = item.getApplyAmount() == null ? BigDecimal.ZERO : item.getApplyAmount(); BigDecimal amount = item.getApplyAmount() == null ? BigDecimal.ZERO : item.getApplyAmount();
if (amount.compareTo(BigDecimal.ZERO) == 0) { if (amount.compareTo(BigDecimal.ZERO) == 0) {
markItemFailed(item.getId(), "applyAmount=0"); markItemFailed(item.getId(), "applyAmount=0");
@@ -241,7 +245,7 @@ public class EarningsDeductionBatchServiceImpl
EarningsAdjustmentReasonType.MANUAL, EarningsAdjustmentReasonType.MANUAL,
batch.getReasonDescription(), batch.getReasonDescription(),
idempotencyKey, idempotencyKey,
LocalDateTime.now()); null);
if (!StringUtils.hasText(item.getAdjustmentId())) { if (!StringUtils.hasText(item.getAdjustmentId())) {
EarningsDeductionItemEntity patch = new EarningsDeductionItemEntity(); EarningsDeductionItemEntity patch = new EarningsDeductionItemEntity();

View File

@@ -0,0 +1,130 @@
package com.starry.admin.modules.withdraw.service.impl;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.api.AbstractApiTest;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.withdraw.entity.EarningsDeductionBatchEntity;
import com.starry.admin.modules.withdraw.entity.EarningsDeductionBatchLogEntity;
import com.starry.admin.modules.withdraw.entity.EarningsDeductionItemEntity;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.enums.EarningsDeductionOperationType;
import com.starry.admin.modules.withdraw.enums.EarningsDeductionRuleType;
import com.starry.admin.modules.withdraw.enums.EarningsSourceType;
import com.starry.admin.modules.withdraw.mapper.EarningsDeductionBatchLogMapper;
import com.starry.admin.modules.withdraw.mapper.EarningsDeductionItemMapper;
import com.starry.admin.modules.withdraw.service.IEarningsAdjustmentService;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
class EarningsDeductionBatchServiceImplIdempotencyIntegrationTest extends AbstractApiTest {
@Autowired
private EarningsDeductionBatchServiceImpl batchService;
@Autowired
private EarningsDeductionItemMapper itemMapper;
@Autowired
private EarningsDeductionBatchLogMapper logMapper;
@Autowired
private IEarningsAdjustmentService adjustmentService;
@Autowired
private IEarningsService earningsService;
private String batchId;
private String clerkId;
@BeforeEach
void setUp() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
clerkId = IdUtils.getUuid();
}
@AfterEach
void tearDown() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
if (batchId == null) {
return;
}
List<EarningsDeductionItemEntity> items = itemMapper.selectList(Wrappers.lambdaQuery(EarningsDeductionItemEntity.class)
.eq(EarningsDeductionItemEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.eq(EarningsDeductionItemEntity::getBatchId, batchId)
.eq(EarningsDeductionItemEntity::getDeleted, false));
List<String> adjustmentIds = items.stream()
.map(EarningsDeductionItemEntity::getAdjustmentId)
.filter(id -> id != null && !id.isBlank())
.distinct()
.collect(Collectors.toList());
if (!adjustmentIds.isEmpty()) {
earningsService.lambdaUpdate()
.eq(EarningsLineEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.eq(EarningsLineEntity::getClerkId, clerkId)
.eq(EarningsLineEntity::getSourceType, EarningsSourceType.ADJUSTMENT)
.in(EarningsLineEntity::getSourceId, adjustmentIds)
.remove();
adjustmentService.removeByIds(adjustmentIds);
}
itemMapper.delete(Wrappers.lambdaQuery(EarningsDeductionItemEntity.class)
.eq(EarningsDeductionItemEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.eq(EarningsDeductionItemEntity::getBatchId, batchId));
logMapper.delete(Wrappers.lambdaQuery(EarningsDeductionBatchLogEntity.class)
.eq(EarningsDeductionBatchLogEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.eq(EarningsDeductionBatchLogEntity::getBatchId, batchId));
batchService.removeById(batchId);
}
@Test
void applyOnce_canBeTriggeredTwiceWithoutBreakingIdempotency() {
String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID;
LocalDateTime begin = LocalDateTime.now().minusDays(1);
LocalDateTime end = LocalDateTime.now();
String idempotencyKey = "e2e-deduct-idem-" + UUID.randomUUID();
EarningsDeductionBatchEntity batch = batchService.createOrGetProcessing(
tenantId,
List.of(clerkId),
begin,
end,
EarningsDeductionRuleType.FIXED,
new BigDecimal("1.00"),
EarningsDeductionOperationType.PUNISHMENT,
"idempotency test",
idempotencyKey);
assertNotNull(batch);
batchId = batch.getId();
batchService.applyOnce(batchId);
EarningsDeductionItemEntity item = itemMapper.selectOne(Wrappers.lambdaQuery(EarningsDeductionItemEntity.class)
.eq(EarningsDeductionItemEntity::getTenantId, tenantId)
.eq(EarningsDeductionItemEntity::getBatchId, batchId)
.eq(EarningsDeductionItemEntity::getClerkId, clerkId)
.last("limit 1"));
assertNotNull(item);
assertTrue(item.getAdjustmentId() != null && !item.getAdjustmentId().isBlank());
assertDoesNotThrow(() -> batchService.applyOnce(batchId));
}
}