fix(firstleg): cancel coload groups by inbound code
This commit is contained in:
+6
-7
@@ -6,7 +6,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BusinessSummary;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelByInboundCodeRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CreateRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
|
||||
import com.baiyue.firstleg.loading.coload.service.ColoadGroupService;
|
||||
@@ -15,7 +15,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
@@ -52,19 +51,19 @@ public class PlatformColoadGroupController {
|
||||
user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/{groupId}/cancel")
|
||||
@PostMapping("/by-inbound-code/{inboundCode}/cancel")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
@BusinessAudit(event = "COLOAD_GROUP_CANCEL", resourceType = "COLOAD_GROUP",
|
||||
resourceId = "#groupId", description = "整组取消同柜不可拆组")
|
||||
resourceId = "#inboundCode", description = "按入仓码整组取消同柜不可拆组")
|
||||
@Operation(summary = "整组取消同柜不可拆组",
|
||||
description = "仅 ACTIVE 组允许取消;不提供单成员编辑,修改成员必须取消后重建。")
|
||||
public ApiResponse<GroupSnapshot> cancel(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Min(1) long groupId,
|
||||
@PathVariable @Size(min = 1, max = 40) String inboundCode,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody CancelRequest request) {
|
||||
@Valid @RequestBody CancelByInboundCodeRequest request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.cancel(groupId, request.expectedVersion(), request.reason(),
|
||||
return ApiResponse.success(service.cancelByInboundCode(inboundCode, request.reason(),
|
||||
idempotencyKey, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -18,8 +18,7 @@ public final class ColoadGroupModels {
|
||||
) {
|
||||
}
|
||||
|
||||
public record CancelRequest(
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
public record CancelByInboundCodeRequest(
|
||||
@Size(max = 500) String reason
|
||||
) {
|
||||
}
|
||||
|
||||
+35
-11
@@ -65,25 +65,30 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupSnapshot cancel(long groupId, int expectedVersion, String reason,
|
||||
String idempotencyKey, long operatorId, String operatorName) {
|
||||
if (groupId <= 0 || expectedVersion < 0) throw badRequest("组ID或版本不合法");
|
||||
public GroupSnapshot cancelByInboundCode(String inboundCode, String reason,
|
||||
String idempotencyKey, long operatorId,
|
||||
String operatorName) {
|
||||
String code = normalizedInboundCode(inboundCode);
|
||||
String key = key(idempotencyKey);
|
||||
String normalizedReason = trim(reason);
|
||||
String requestHash = sha256("CANCEL|" + groupId + '|' + expectedVersion + '|'
|
||||
String requestHash = sha256("CANCEL_BY_INBOUND_CODE|" + code + '|'
|
||||
+ Objects.toString(normalizedReason, ""));
|
||||
CommandClaim claim = claim(operatorId, "CANCEL", key, requestHash);
|
||||
if (claim.replayGroupId() != null) return requiredSnapshot(claim.replayGroupId());
|
||||
|
||||
long groupId = requiredCurrentGroupId(code);
|
||||
GroupRow group = requiredLocked(groupId);
|
||||
if (!"ACTIVE".equals(group.getGroupStatus())) {
|
||||
throw conflict("只有未锁定的同柜不可拆组可以整组取消");
|
||||
}
|
||||
if (!Objects.equals(group.getVersion(), expectedVersion)
|
||||
|| mapper.cancel(groupId, expectedVersion, operatorId, trim(operatorName)) != 1) {
|
||||
GroupSnapshot before = snapshot(group);
|
||||
int expectedVersion = group.getVersion();
|
||||
if (mapper.cancel(groupId, expectedVersion, operatorId, trim(operatorName)) != 1) {
|
||||
throw conflict("组版本已变化,请刷新后重试");
|
||||
}
|
||||
mapper.closeMembers(groupId);
|
||||
if (mapper.closeMembers(groupId) != before.members().size()) {
|
||||
throw conflict("同柜不可拆组成员状态已变化,请刷新后重试");
|
||||
}
|
||||
history(groupId, "CANCEL", "ACTIVE", "CANCELLED", null, key,
|
||||
normalizedReason, operatorId, operatorName);
|
||||
complete(claim.commandId(), groupId);
|
||||
@@ -92,8 +97,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BusinessSummary queryByInboundCode(String inboundCode) {
|
||||
String code = trim(inboundCode);
|
||||
if (code == null || code.length() > 40) throw badRequest("inboundCode不能为空且不能超过40字符");
|
||||
String code = normalizedInboundCode(inboundCode);
|
||||
var groups = mapper.groupsByInboundCode(code).stream()
|
||||
.filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus()))
|
||||
.toList();
|
||||
@@ -118,6 +122,24 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
||||
return new BusinessSummary(status, members.size(), members);
|
||||
}
|
||||
|
||||
private long requiredCurrentGroupId(String inboundCode) {
|
||||
var groups = mapper.groupsByInboundCode(inboundCode).stream()
|
||||
.filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus()))
|
||||
.toList();
|
||||
long distinctGroups = groups.stream().map(row -> row.getGroupId()).distinct().limit(2).count();
|
||||
if (distinctGroups == 0) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入仓码未关联有效同柜不可拆组");
|
||||
}
|
||||
if (distinctGroups > 1) throw conflict("入仓码关联多个有效同柜不可拆组");
|
||||
return groups.get(0).getGroupId();
|
||||
}
|
||||
|
||||
private static String normalizedInboundCode(String inboundCode) {
|
||||
String code = trim(inboundCode);
|
||||
if (code == null || code.length() > 40) throw badRequest("inboundCode不能为空且不能超过40字符");
|
||||
return code;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<InternalOrderGroupInfo> queryInternalByOrderIds(List<Long> requestedOrderIds) {
|
||||
List<Long> orderIds = normalizedOrderIds(requestedOrderIds, 1, 200);
|
||||
@@ -331,8 +353,10 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
||||
|
||||
private void history(long groupId, String action, String fromStatus, String toStatus, Long planId,
|
||||
String operationId, String remark, long operatorId, String operatorName) {
|
||||
mapper.insertHistory(IdWorker.getId(), groupId, action, fromStatus, toStatus, planId,
|
||||
operationId, trim(remark), operatorId, trim(operatorName));
|
||||
if (mapper.insertHistory(IdWorker.getId(), groupId, action, fromStatus, toStatus, planId,
|
||||
operationId, trim(remark), operatorId, trim(operatorName)) != 1) {
|
||||
throw conflict("同柜不可拆组审计历史写入冲突");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Long> normalizedOrderIds(List<Long> source, int minimum, int maximum) {
|
||||
|
||||
@@ -20,18 +20,18 @@ paths:
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/ColoadGroupResponse'}
|
||||
'403': {description: 缺少智能排柜分配权限}
|
||||
/api/platform/v1/first-leg/coload-groups/{groupId}/cancel:
|
||||
/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}/cancel:
|
||||
post:
|
||||
summary: 整组取消同柜不可拆组
|
||||
description: 仅 ACTIVE 组允许取消;不提供单成员编辑,修改成员必须取消后重建。
|
||||
description: 使用任一成员入仓码整组取消;仅 ACTIVE 组允许取消,不提供单成员编辑。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
- {name: groupId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundCode, in: path, required: true, schema: {type: string, minLength: 1, maxLength: 40}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CancelColoadGroupRequest'}
|
||||
schema: {$ref: '#/components/schemas/CancelColoadGroupByInboundCodeRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/ColoadGroupResponse'}
|
||||
'403': {description: 缺少智能排柜分配权限}
|
||||
@@ -98,12 +98,10 @@ components:
|
||||
maxItems: 100
|
||||
uniqueItems: true
|
||||
items: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
CancelColoadGroupRequest:
|
||||
CancelColoadGroupByInboundCodeRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [expectedVersion]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
reason: {type: string, maxLength: 500, nullable: true}
|
||||
ColoadGroupMember:
|
||||
type: object
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ package com.baiyue.firstleg.loading.coload;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.coload.controller.PlatformColoadGroupController;
|
||||
import com.baiyue.firstleg.loading.coload.controller.InternalColoadGroupController;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelByInboundCodeRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CreateRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.InternalBatchQueryRequest;
|
||||
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
@@ -25,8 +25,8 @@ class ColoadGroupContractTest {
|
||||
assertThat(base.value()).containsExactly("/api/platform/v1/first-leg/coload-groups");
|
||||
|
||||
assertEndpoint("create", "", AuthenticatedUser.class, String.class, CreateRequest.class);
|
||||
assertEndpoint("cancel", "/{groupId}/cancel", AuthenticatedUser.class, long.class,
|
||||
String.class, CancelRequest.class);
|
||||
assertEndpoint("cancel", "/by-inbound-code/{inboundCode}/cancel", AuthenticatedUser.class,
|
||||
String.class, String.class, CancelByInboundCodeRequest.class);
|
||||
Method read = PlatformColoadGroupController.class.getDeclaredMethod(
|
||||
"byInboundCode", AuthenticatedUser.class, String.class);
|
||||
assertThat(read.getAnnotation(GetMapping.class).value())
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ class ColoadGroupOpenApiContractTest {
|
||||
|
||||
assertThat(paths).containsKeys(
|
||||
"/api/platform/v1/first-leg/coload-groups",
|
||||
"/api/platform/v1/first-leg/coload-groups/{groupId}/cancel",
|
||||
"/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}/cancel",
|
||||
"/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}");
|
||||
assertThat(paths).doesNotContainKey("/api/platform/v1/first-leg/coload-groups/batch-query");
|
||||
assertThat(source)
|
||||
@@ -35,7 +35,6 @@ class ColoadGroupOpenApiContractTest {
|
||||
.contains("pattern: '^[1-9][0-9]*$'")
|
||||
.contains("enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]")
|
||||
.contains("Idempotency-Key")
|
||||
.contains("expectedVersion")
|
||||
.contains("fullPackageCount")
|
||||
.contains("platform:firstleg:smart-loading:assign")
|
||||
.contains("platform:firstleg:smart-loading:list")
|
||||
@@ -45,6 +44,7 @@ class ColoadGroupOpenApiContractTest {
|
||||
.doesNotContain("destinationType")
|
||||
.doesNotContain("destinationWarehouseId")
|
||||
.doesNotContain("destinationCode")
|
||||
.doesNotContain("expectedVersion")
|
||||
.doesNotContain("platform:firstleg:coload");
|
||||
assertThat(summaryProperties).containsOnlyKeys("status", "memberCount", "members");
|
||||
assertThat(memberProperties).containsOnlyKeys("inboundCode");
|
||||
|
||||
+90
-2
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.loading.coload;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadGroupMapper;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.GroupRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.CommandRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundCandidateRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.MemberRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundGroupLookupRow;
|
||||
@@ -25,6 +26,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -192,11 +194,13 @@ class ColoadGroupServiceTest {
|
||||
|
||||
@Test
|
||||
void lockedGroupCannotBeCancelled() {
|
||||
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "LOCKED")));
|
||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), eq("cancel-key"), anyString()))
|
||||
.thenReturn(1);
|
||||
when(mapper.lockGroup(500L)).thenReturn(group(500L, "LOCKED", 700L, 2));
|
||||
|
||||
assertThatThrownBy(() -> service.cancel(500L, 2, "change", "cancel-key", 7L, "operator"))
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "change", "cancel-key", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("未锁定");
|
||||
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
|
||||
@@ -204,6 +208,7 @@ class ColoadGroupServiceTest {
|
||||
|
||||
@Test
|
||||
void activeGroupCancelsAsAWholeAndReleasesEveryMembership() {
|
||||
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "ACTIVE")));
|
||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), eq("cancel-all"), anyString()))
|
||||
.thenReturn(1);
|
||||
when(mapper.lockGroup(500L)).thenReturn(group(500L, "ACTIVE", null, 2));
|
||||
@@ -217,10 +222,73 @@ class ColoadGroupServiceTest {
|
||||
when(mapper.members(500L)).thenReturn(List.of(
|
||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
||||
|
||||
var result = service.cancel(500L, 2, "rebuild", "cancel-all", 7L, "operator");
|
||||
var result = service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "cancel-all", 7L, "operator");
|
||||
|
||||
assertThat(result.status()).isEqualTo("CANCELLED");
|
||||
verify(mapper).closeMembers(500L);
|
||||
verify(mapper).insertHistory(anyLong(), eq(500L), eq("CANCEL"), eq("ACTIVE"),
|
||||
eq("CANCELLED"), isNull(), eq("cancel-all"), eq("rebuild"), eq(7L), eq("operator"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelReplayReturnsOriginalResultWithoutResolvingOrMutatingCurrentGroup() {
|
||||
CommandRow command = command("SUCCESS", 500L);
|
||||
when(mapper.command(7L, "CANCEL", "cancel-replay")).thenReturn(command);
|
||||
when(mapper.group(500L)).thenReturn(group(500L, "CANCELLED", null, 3));
|
||||
when(mapper.members(500L)).thenReturn(List.of(
|
||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
||||
|
||||
var result = service.cancelByInboundCode(
|
||||
" IN-A ", "rebuild", "cancel-replay", 7L, "operator");
|
||||
|
||||
assertThat(result.status()).isEqualTo("CANCELLED");
|
||||
verify(mapper, never()).groupsByInboundCode(anyString());
|
||||
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
|
||||
verify(mapper, never()).insertHistory(anyLong(), anyLong(), anyString(), any(), anyString(),
|
||||
any(), anyString(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelReplayRejectsDifferentPayloadAndConcurrentProcessing() {
|
||||
CommandRow differentPayload = command("SUCCESS", 500L);
|
||||
differentPayload.setRequestHash("different-hash");
|
||||
when(mapper.command(7L, "CANCEL", "same-key")).thenReturn(differentPayload);
|
||||
when(mapper.command(7L, "CANCEL", "processing-key"))
|
||||
.thenReturn(command("PROCESSING", null));
|
||||
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "changed", "same-key", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "processing-key", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("正在处理中");
|
||||
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentStateChangeOrPartialMemberCloseRollsBackWithoutAuditCompletion() {
|
||||
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "ACTIVE")));
|
||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), anyString(), anyString()))
|
||||
.thenReturn(1);
|
||||
when(mapper.lockGroup(500L))
|
||||
.thenReturn(group(500L, "CANCELLED", null, 3), group(500L, "ACTIVE", null, 2));
|
||||
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "lost-race", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("未锁定");
|
||||
|
||||
when(mapper.members(500L)).thenReturn(List.of(
|
||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
||||
when(mapper.cancel(500L, 2, 7L, "operator")).thenReturn(1);
|
||||
when(mapper.closeMembers(500L)).thenReturn(1);
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "partial-close", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("成员状态已变化");
|
||||
|
||||
verify(mapper, never()).insertHistory(anyLong(), anyLong(), eq("CANCEL"), any(), anyString(),
|
||||
any(), anyString(), any(), anyLong(), any());
|
||||
verify(mapper, never()).completeCommand(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -237,6 +305,8 @@ class ColoadGroupServiceTest {
|
||||
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50); second.setActiveGroupId(500L);
|
||||
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(first, fragment, second));
|
||||
when(mapper.lockToPlan(500L, 700L, 7L, "operator")).thenReturn(1);
|
||||
when(mapper.insertHistory(anyLong(), eq(500L), eq("LOCK"), eq("ACTIVE"), eq("LOCKED"),
|
||||
eq(700L), eq("assign-whole"), any(), eq(7L), eq("operator"))).thenReturn(1);
|
||||
when(mapper.completeCommand(anyLong(), eq(500L))).thenReturn(1);
|
||||
when(mapper.group(500L)).thenReturn(group(500L, "LOCKED", 700L, 1));
|
||||
|
||||
@@ -318,6 +388,24 @@ class ColoadGroupServiceTest {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static CommandRow command(String status, Long resultGroupId) {
|
||||
CommandRow value = new CommandRow();
|
||||
value.setId(900L);
|
||||
value.setRequestHash(sha256("CANCEL_BY_INBOUND_CODE|IN-A|rebuild"));
|
||||
value.setCommandStatus(status);
|
||||
value.setResultGroupId(resultGroupId);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
|
||||
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static org.assertj.core.groups.Tuple tuple(Object... values) {
|
||||
return org.assertj.core.groups.Tuple.tuple(values);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user