实现M002 V4持久化基线
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user