feat: add ERP push failure recovery
This commit is contained in:
@@ -292,6 +292,10 @@ class ShadowV3OperationsPackageTest {
|
||||
CmdResult result = runProcess(new ProcessBuilder(
|
||||
"git", "clone", "--quiet", root.toString(), destination.toString()), 60);
|
||||
assertThat(result.exitCode()).as(result.output()).isZero();
|
||||
CmdResult branch = runProcess(new ProcessBuilder(
|
||||
"git", "-C", destination.toString(), "branch", "-m",
|
||||
"test/ft546wms-shadow-v3-20260903"), 15);
|
||||
assertThat(branch.exitCode()).as(branch.output()).isZero();
|
||||
}
|
||||
|
||||
private String gitHead(Path repository) throws Exception {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ruoyi.erp.push.mapper;
|
||||
|
||||
import com.ruoyi.erp.push.domain.ErpPushException;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ErpPushExceptionMapperSqlTest {
|
||||
private static final String RESOURCE = "mapper/erp/ErpPushExceptionMapper.xml";
|
||||
private static final String NAMESPACE =
|
||||
"com.ruoyi.erp.push.mapper.ErpPushExceptionMapper.";
|
||||
private static final Pattern FROM_TABLE = Pattern.compile("\\bfrom\\s+([a-z0-9_]+)");
|
||||
private static final String RECENT_MONTH_PREDICATE =
|
||||
"try_convert(datetime2, send_time, 120) >= dateadd(month, -1, sysdatetime())";
|
||||
|
||||
@Test
|
||||
void failureListReadsOnlyRecentCurrentPushDocumentTables() throws Exception {
|
||||
String sql = boundSql("selectFailureList");
|
||||
|
||||
assertThat(fromTables(sql)).containsExactly(
|
||||
"in_stockin_back",
|
||||
"out_stockout_back",
|
||||
"move_msg");
|
||||
assertThat(occurrences(sql, RECENT_MONTH_PREDICATE)).isEqualTo(3);
|
||||
assertThat(sql).doesNotContain("request_log", "history", "_finish");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryUpdatesOnlyRequeueCurrentFailedDocumentsFromTheRecentMonth() throws Exception {
|
||||
assertThat(boundSql("requeueInboundHeader"))
|
||||
.contains("update in_stockin_back", "set send_status = '0'", "send_status = '2'",
|
||||
"stock_source = '1'", RECENT_MONTH_PREDICATE);
|
||||
assertThat(boundSql("requeueInboundDetails"))
|
||||
.contains("update in_stockin_detail_back", "set send_status = '0'", "send_status = '2'",
|
||||
RECENT_MONTH_PREDICATE);
|
||||
assertThat(boundSql("requeueOutboundHeader"))
|
||||
.contains("update out_stockout_back", "set send_status = '0'", "send_status = '2'",
|
||||
"stockout_source = '1'", RECENT_MONTH_PREDICATE);
|
||||
assertThat(boundSql("requeueOutboundDetails"))
|
||||
.contains("update out_stockout_detail_back", "set send_status = '0'", "send_status = '2'",
|
||||
RECENT_MONTH_PREDICATE);
|
||||
assertThat(boundSql("requeueMove"))
|
||||
.contains("update move_msg", "set status = '0'", "status = '2'", RECENT_MONTH_PREDICATE);
|
||||
}
|
||||
|
||||
private String boundSql(String statementId) throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
try (InputStream stream = Objects.requireNonNull(
|
||||
getClass().getClassLoader().getResourceAsStream(RESOURCE), RESOURCE)) {
|
||||
new XMLMapperBuilder(stream, configuration, RESOURCE, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
return configuration.getMappedStatement(NAMESPACE + statementId)
|
||||
.getBoundSql(new ErpPushException())
|
||||
.getSql()
|
||||
.replaceAll("\\s+", " ")
|
||||
.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private List<String> fromTables(String sql) {
|
||||
List<String> tables = new ArrayList<>();
|
||||
Matcher matcher = FROM_TABLE.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
tables.add(matcher.group(1));
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
private int occurrences(String value, String token) {
|
||||
return (value.length() - value.replace(token, "").length()) / token.length();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.ruoyi.erp.push.service;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.erp.push.domain.ErpPushRetryRequest;
|
||||
import com.ruoyi.erp.push.mapper.ErpPushExceptionMapper;
|
||||
import com.ruoyi.erp.push.service.impl.ErpPushExceptionServiceImpl;
|
||||
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 static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ErpPushExceptionServiceTest {
|
||||
@Mock
|
||||
private ErpPushExceptionMapper mapper;
|
||||
|
||||
private ErpPushExceptionServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ErpPushExceptionServiceImpl(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requeuesInboundHeaderAndDetailsTogether() {
|
||||
when(mapper.requeueInboundHeader("IN-GUID")).thenReturn(1);
|
||||
when(mapper.requeueInboundDetails("IN-GUID")).thenReturn(2);
|
||||
|
||||
assertTrue(service.retry(request("INBOUND", "IN-GUID")));
|
||||
|
||||
verify(mapper).requeueInboundHeader("IN-GUID");
|
||||
verify(mapper).requeueInboundDetails("IN-GUID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInboundRetryWhenNoFailedDetailsExist() {
|
||||
when(mapper.requeueInboundHeader("IN-GUID")).thenReturn(1);
|
||||
when(mapper.requeueInboundDetails("IN-GUID")).thenReturn(0);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.retry(request("INBOUND", "IN-GUID")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleFailureIsNotRequeuedTwice() {
|
||||
when(mapper.requeueOutboundHeader("OUT-GUID")).thenReturn(0);
|
||||
|
||||
assertFalse(service.retry(request("OUTBOUND", "OUT-GUID")));
|
||||
|
||||
verify(mapper, never()).requeueOutboundDetails("OUT-GUID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void requeuesOutboundHeaderAndDetailsTogether() {
|
||||
when(mapper.requeueOutboundHeader("OUT-GUID")).thenReturn(1);
|
||||
when(mapper.requeueOutboundDetails("OUT-GUID")).thenReturn(3);
|
||||
|
||||
assertTrue(service.retry(request("OUTBOUND", "OUT-GUID")));
|
||||
|
||||
verify(mapper).requeueOutboundDetails("OUT-GUID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void requeuesMoveWithoutTouchingDocumentDetails() {
|
||||
when(mapper.requeueMove("MOVE-ID")).thenReturn(1);
|
||||
|
||||
assertTrue(service.retry(request("MOVE", "MOVE-ID")));
|
||||
|
||||
verify(mapper).requeueMove("MOVE-ID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownDocumentType() {
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.retry(request("OTHER", "ID")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBlankDocumentIdentityBeforeCallingMapper() {
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.retry(request("INBOUND", " ")));
|
||||
|
||||
verify(mapper, never()).requeueInboundHeader(org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
|
||||
private ErpPushRetryRequest request(String documentType, String recordId) {
|
||||
ErpPushRetryRequest request = new ErpPushRetryRequest();
|
||||
request.setDocumentType(documentType);
|
||||
request.setRecordId(recordId);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user