实现M002 V4持久化基线

This commit is contained in:
andy
2026-07-19 00:12:40 +07:00
parent 3d9055dd6b
commit e6fbd7a111
29 changed files with 1563 additions and 33 deletions

View File

@@ -0,0 +1,274 @@
package cn.nianxx.thhotel.workflows.reservation.repository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4NotificationStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4OrderTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4TargetResolutionStatus;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(
classes = ThHotelApplication.class,
properties = {
"spring.datasource.url=jdbc:h2:mem:m002_v4_repository;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
"auth.bootstrap.admin.username=",
"auth.bootstrap.admin.password=",
"auth.bootstrap.default-hotel-id="
})
@ActiveProfiles("test")
class MybatisReservationV4WorkflowRepositoryTest {
private static final String HOTEL_ID = "HOTEL-TEST";
@Autowired
private ReservationV4WorkflowRepository workflowRepository;
@Autowired
private ReservationV4SourceNotificationRepository sourceNotificationRepository;
@Test
void shouldCreateOrderTaskIdempotentlyBySourceMessageAndOrderRef() {
ReservationV4OrderTaskDraft draft = orderTaskDraft(1001L, "order-1", 501L, now());
ReservationV4OrderTaskSnapshot first = workflowRepository.findOrCreateOrderTask(draft);
ReservationV4OrderTaskSnapshot second = workflowRepository.findOrCreateOrderTask(draft);
assertThat(second.id()).isEqualTo(first.id());
assertThat(workflowRepository.findOrderTaskBySourceMessageAndOrderRef(HOTEL_ID, 1001L, "order-1"))
.get()
.satisfies(snapshot -> {
assertThat(snapshot.hotelId()).isEqualTo(HOTEL_ID);
assertThat(snapshot.orderRef()).isEqualTo("order-1");
assertThat(snapshot.orderContextIndex()).isEqualTo(1);
assertThat(snapshot.orderTaskStatus()).isEqualTo(ReservationV4OrderTaskStatus.OPEN.name());
assertThat(snapshot.targetResolutionStatus())
.isEqualTo(ReservationV4TargetResolutionStatus.RESOLVED.name());
});
}
@Test
void shouldFindOrderTasksBySourceMessageInOrderContextIndexOrder() {
workflowRepository.findOrCreateOrderTask(orderTaskDraft(1010L, "order-later", 510L, 2, now()));
workflowRepository.findOrCreateOrderTask(orderTaskDraft(1010L, "order-earlier", 511L, 1, now()));
assertThat(workflowRepository.findOrderTasksBySourceMessageId(HOTEL_ID, 1010L))
.extracting(ReservationV4OrderTaskSnapshot::orderRef)
.containsExactly("order-earlier", "order-later");
}
@Test
void shouldStoreNonEventCardWithZeroSourceEventIndexAndRejectDuplicateSlot() {
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(
orderTaskDraft(1002L, "order-2", 502L, now()));
ReservationV4TaskCardDraft draft = sourceMessageDisplayCardDraft(orderTask.id(), 1002L, now());
ReservationV4TaskCardSnapshot card = workflowRepository.insertTaskCard(draft);
assertThat(card.sourceEventIndex()).isZero();
assertThat(card.version()).isZero();
assertThatThrownBy(() -> workflowRepository.insertTaskCard(draft))
.isInstanceOf(DataIntegrityViolationException.class);
}
@Test
void shouldRejectEventCardWithoutPositiveSourceEventIndex() {
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(
orderTaskDraft(1011L, "order-11", 511L, now()));
assertThatThrownBy(() -> workflowRepository.insertTaskCard(
eventCardDraftWithoutSourceEventIndex(orderTask.id(), 1011L, 2011L, now())))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("source_event_index");
}
@Test
void shouldUpdateTaskCardStatusWithExpectedVersion() {
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(
orderTaskDraft(1003L, "order-3", 503L, now()));
ReservationV4TaskCardSnapshot card = workflowRepository.insertTaskCard(
businessCardDraft(orderTask.id(), 1003L, 2003L, now()));
LocalDateTime updatedAt = now().plusMinutes(1);
boolean updated = workflowRepository.updateTaskCardStatusWithVersion(
HOTEL_ID,
card.id(),
0L,
ReservationV4CardStatus.CONFIRMED.name(),
updatedAt);
assertThat(updated).isTrue();
ReservationV4TaskCardSnapshot afterUpdate = workflowRepository.findTaskCardById(HOTEL_ID, card.id()).orElseThrow();
assertThat(afterUpdate.cardStatus()).isEqualTo(ReservationV4CardStatus.CONFIRMED.name());
assertThat(afterUpdate.version()).isEqualTo(1L);
assertThat(workflowRepository.updateTaskCardStatusWithVersion(
HOTEL_ID,
card.id(),
0L,
ReservationV4CardStatus.PENDING_CONFIRM.name(),
updatedAt.plusMinutes(1))).isFalse();
}
@Test
void shouldCreateSourceNotificationIdempotentlyAndAckWithVersion() {
ReservationV4SourceNotificationDraft draft = sourceNotificationDraft(1004L, 704L, now());
ReservationV4SourceNotificationSnapshot first = sourceNotificationRepository.findOrCreateSourceNotification(draft);
ReservationV4SourceNotificationSnapshot second = sourceNotificationRepository.findOrCreateSourceNotification(draft);
LocalDateTime ackAt = now().plusMinutes(2);
assertThat(second.id()).isEqualTo(first.id());
assertThat(first.notificationStatus()).isEqualTo(ReservationV4NotificationStatus.ACK_REQUIRED.name());
assertThat(first.version()).isZero();
assertThat(sourceNotificationRepository.updateNotificationStatusWithVersion(
HOTEL_ID,
first.id(),
0L,
ReservationV4NotificationStatus.ACKED.name(),
"user-1",
ackAt)).isTrue();
assertThat(sourceNotificationRepository.updateNotificationStatusWithVersion(
HOTEL_ID,
first.id(),
0L,
ReservationV4NotificationStatus.ACK_REQUIRED.name(),
"user-2",
ackAt.plusMinutes(1))).isFalse();
ReservationV4SourceNotificationSnapshot afterAck =
sourceNotificationRepository.findSourceNotificationById(HOTEL_ID, first.id()).orElseThrow();
assertThat(afterAck.notificationStatus()).isEqualTo(ReservationV4NotificationStatus.ACKED.name());
assertThat(afterAck.ackBy()).isEqualTo("user-1");
assertThat(afterAck.ackAt()).isEqualTo(ackAt);
assertThat(afterAck.version()).isEqualTo(1L);
}
private ReservationV4OrderTaskDraft orderTaskDraft(
Long sourceMessageId,
String orderRef,
Long orderId,
LocalDateTime now) {
return orderTaskDraft(sourceMessageId, orderRef, orderId, 1, now);
}
private ReservationV4OrderTaskDraft orderTaskDraft(
Long sourceMessageId,
String orderRef,
Long orderId,
Integer orderContextIndex,
LocalDateTime now) {
return new ReservationV4OrderTaskDraft(
HOTEL_ID,
sourceMessageId,
9001L,
orderRef,
orderContextIndex,
orderId,
"GROUP",
"GROUP_CODE",
"GRP-" + orderRef,
ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4OrderTaskStatus.OPEN.name(),
now.minusMinutes(5),
now);
}
private ReservationV4TaskCardDraft sourceMessageDisplayCardDraft(
Long orderTaskId,
Long sourceMessageId,
LocalDateTime now) {
return new ReservationV4TaskCardDraft(
HOTEL_ID,
orderTaskId,
sourceMessageId,
null,
ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(),
null,
null,
10,
ReservationV4CardStatus.READONLY.name(),
null,
"{\"source\":\"ai\"}",
"{\"display\":\"mail\"}",
null,
now);
}
private ReservationV4TaskCardDraft eventCardDraftWithoutSourceEventIndex(
Long orderTaskId,
Long sourceMessageId,
Long transitionId,
LocalDateTime now) {
return new ReservationV4TaskCardDraft(
HOTEL_ID,
orderTaskId,
sourceMessageId,
transitionId,
ReservationV4CardType.ROOM_INFORMATION.name(),
"NEW_BOOKING",
null,
30,
ReservationV4CardStatus.PENDING_CONFIRM.name(),
null,
"{\"event\":\"new_booking\"}",
"{\"display\":\"room\"}",
null,
now);
}
private ReservationV4TaskCardDraft businessCardDraft(
Long orderTaskId,
Long sourceMessageId,
Long transitionId,
LocalDateTime now) {
return new ReservationV4TaskCardDraft(
HOTEL_ID,
orderTaskId,
sourceMessageId,
transitionId,
ReservationV4CardType.ROOM_INFORMATION.name(),
"NEW_BOOKING",
1,
30,
ReservationV4CardStatus.PENDING_CONFIRM.name(),
null,
"{\"event\":\"new_booking\"}",
"{\"display\":\"room\"}",
null,
now);
}
private ReservationV4SourceNotificationDraft sourceNotificationDraft(
Long sourceMessageId,
Long transitionId,
LocalDateTime now) {
return new ReservationV4SourceNotificationDraft(
HOTEL_ID,
sourceMessageId,
9004L,
transitionId,
"S10",
ReservationV4NotificationStatus.ACK_REQUIRED.name(),
"{\"route_code\":\"S10\"}",
now.minusMinutes(3),
now);
}
private LocalDateTime now() {
return LocalDateTime.of(2026, 7, 18, 8, 30, 0);
}
}