feat:Add check out API

This commit is contained in:
蔡旻峰
2026-09-22 10:42:59 +08:00
parent 7562ccceec
commit b5eac229cb
6 changed files with 217 additions and 0 deletions
@@ -45,6 +45,9 @@ public class EsignAuditLogService {
public static final String EVT_OHIP_PUT_PROFILE = "OHIP_PUT_PROFILE"; public static final String EVT_OHIP_PUT_PROFILE = "OHIP_PUT_PROFILE";
public static final String EVT_OHIP_POST_AUTO_ROOM_ASSIGNMENT = "OHIP_POST_AUTO_ROOM_ASSIGNMENT"; public static final String EVT_OHIP_POST_AUTO_ROOM_ASSIGNMENT = "OHIP_POST_AUTO_ROOM_ASSIGNMENT";
public static final String EVT_OHIP_POST_CHECK_IN = "OHIP_POST_CHECK_IN"; public static final String EVT_OHIP_POST_CHECK_IN = "OHIP_POST_CHECK_IN";
public static final String EVT_OHIP_POST_CHECK_OUT_SEARCH = "OHIP_POST_CHECK_OUT_SEARCH";
public static final String EVT_OHIP_POST_CHECK_OUT_PAYMENT = "OHIP_POST_CHECK_OUT_PAYMENT";
public static final String EVT_OHIP_POST_CHECK_OUT = "OHIP_POST_CHECK_OUT";
public static final String EVT_OHIP_PUT_STAY_EXTENSION = "OHIP_PUT_STAY_EXTENSION"; public static final String EVT_OHIP_PUT_STAY_EXTENSION = "OHIP_PUT_STAY_EXTENSION";
@Resource(name = "esignAuditLogRepository") @Resource(name = "esignAuditLogRepository")
@@ -2,6 +2,7 @@ package com.sjmresorts.opsapp.booking.reservation.controllers;
import com.sjmresorts.opsapp.esign.common.EsignApiPaths; import com.sjmresorts.opsapp.esign.common.EsignApiPaths;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingCheckInResult; import com.sjmresorts.opsapp.esign.reservation.dtos.BookingCheckInResult;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingCheckOutResult;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingStayExtensionRequest; import com.sjmresorts.opsapp.esign.reservation.dtos.BookingStayExtensionRequest;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingStayExtensionResult; import com.sjmresorts.opsapp.esign.reservation.dtos.BookingStayExtensionResult;
import com.sjmresorts.opsapp.esign.reservation.services.ReservationThirdPartyClient; import com.sjmresorts.opsapp.esign.reservation.services.ReservationThirdPartyClient;
@@ -39,6 +40,19 @@ public class BookingCheckInController {
return ResponseEntity.status(resolveHttpStatus(result.statusCode())).body(result); return ResponseEntity.status(resolveHttpStatus(result.statusCode())).body(result);
} }
@PostMapping("/{reservationId}/check-out")
@Operation(summary = "办理 Booking 自动退房")
public ResponseEntity<BookingCheckOutResult> checkOut(
@PathVariable String reservationId,
@RequestParam(required = false) String hotelId) {
if (StringUtils.isBlank(reservationId)) {
return ResponseEntity.badRequest().body(BookingCheckOutResult.failure(
400, reservationId, null, null, "reservationId required", null, null, null));
}
BookingCheckOutResult result = reservationThirdPartyClient.checkOut(hotelId, reservationId);
return ResponseEntity.status(resolveHttpStatus(result.statusCode())).body(result);
}
@PutMapping("/{reservationId}/stay") @PutMapping("/{reservationId}/stay")
@Operation(summary = "续房并修改 Booking 离店日期") @Operation(summary = "续房并修改 Booking 离店日期")
public ResponseEntity<BookingStayExtensionResult> extendStay( public ResponseEntity<BookingStayExtensionResult> extendStay(
@@ -5,6 +5,7 @@ import com.sjmresorts.opsapp.common.utils.SimpleHttpClient;
import com.sjmresorts.opsapp.esign.audit.services.EsignAuditLogService; import com.sjmresorts.opsapp.esign.audit.services.EsignAuditLogService;
import com.sjmresorts.opsapp.esign.audit.support.ThirdPartyAuditDetailBuilder; import com.sjmresorts.opsapp.esign.audit.support.ThirdPartyAuditDetailBuilder;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingCheckInResult; import com.sjmresorts.opsapp.esign.reservation.dtos.BookingCheckInResult;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingCheckOutResult;
import com.sjmresorts.opsapp.esign.reservation.dtos.BookingStayExtensionResult; import com.sjmresorts.opsapp.esign.reservation.dtos.BookingStayExtensionResult;
import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationThirdPartyResult; import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationThirdPartyResult;
import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult;
@@ -25,6 +26,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
@@ -80,6 +82,18 @@ public class ReservationThirdPartyClient {
@Value("${ops-app.esign.reservation.third-party.booking-stay-extension.path-template:/rsv/v1/hotels/{hotelId}/reservations/{reservationId}}") @Value("${ops-app.esign.reservation.third-party.booking-stay-extension.path-template:/rsv/v1/hotels/{hotelId}/reservations/{reservationId}}")
private String bookingStayExtensionPathTemplate; private String bookingStayExtensionPathTemplate;
@Value("${ops-app.esign.reservation.third-party.booking-check-out.search.path-template:/rsv/v1/hotels/{hotelId}/reservations/searches}")
private String checkOutSearchPathTemplate;
@Value("${ops-app.esign.reservation.third-party.booking-check-out.payment.path-template:/csh/v1/hotels/{hotelId}/reservations/{reservationId}/payments}")
private String checkOutPaymentPathTemplate;
@Value("${ops-app.esign.reservation.third-party.booking-check-out.check-out.path-template:/csh/v1/hotels/{hotelId}/reservations/{reservationId}/checkOuts}")
private String checkOutPathTemplate;
@Value("${ops-app.esign.reservation.third-party.booking-check-out.cashier-id:104}")
private int checkOutCashierId;
@Value("${ops-app.esign.reservation.third-party.file-attachment.path:/med/config/v1/fileAttachments}") @Value("${ops-app.esign.reservation.third-party.file-attachment.path:/med/config/v1/fileAttachments}")
private String fileAttachmentPath; private String fileAttachmentPath;
@@ -311,6 +325,168 @@ public class ReservationThirdPartyClient {
checkInResponse.getStatusCode(), reservationKey, roomId, assignmentSummary, checkInSummary); checkInResponse.getStatusCode(), reservationKey, roomId, assignmentSummary, checkInSummary);
} }
public BookingCheckOutResult checkOut(String hotelId, String reservationId) {
String reservationKey = StringUtils.trimToNull(reservationId);
String ohipHotelId = resolveOhipHotelId(hotelId);
if (StringUtils.isAnyBlank(baseUrl, ohipHotelId, reservationKey)) {
return BookingCheckOutResult.failure(400, reservationId, null, null,
"hotelId and reservationId required", null, null, null);
}
if (!oauthTokenSingleton.isConfigured()) {
return BookingCheckOutResult.failure(503, reservationKey, null, null,
"OHIP OAuth is not configured", null, null, null);
}
JSONObject searchBody = new JSONObject();
JSONArray reservationIdList = new JSONArray();
try {
reservationIdList.add(Long.parseLong(reservationKey));
} catch (NumberFormatException e) {
return BookingCheckOutResult.failure(400, reservationKey, null, null,
"reservationId must be numeric", null, null, null);
}
searchBody.put("reservationIdList", reservationIdList);
HttpResponseResult searchResponse;
try {
searchResponse = executePost(
checkOutSearchPathTemplate.replace("{hotelId}", urlEncodePathSegment(ohipHotelId)),
searchBody,
ohipHotelId,
EsignAuditLogService.EVT_OHIP_POST_CHECK_OUT_SEARCH,
"POST check out reservation search reservationId=" + reservationKey);
} catch (IllegalStateException e) {
return BookingCheckOutResult.failure(502, reservationKey, null, null, e.getMessage(), null, null, null);
}
String searchSummary = truncate(searchResponse.getResponseContent());
if (searchResponse.isTransportError()) {
return BookingCheckOutResult.failure(502, reservationKey, null, null,
StringUtils.defaultIfBlank(searchResponse.getErrorMessage(), "OHIP reservation search transport error"),
searchSummary, null, null);
}
if (!searchResponse.isSuccessful()) {
return BookingCheckOutResult.failure(searchResponse.getStatusCode(), reservationKey, null, null,
extractOhipErrorMessage(searchResponse.bodyOrEmpty(),
"OHIP reservation search returned HTTP " + searchResponse.getStatusCode()),
searchSummary, null, null);
}
JSONObject balance = extractReservationBalance(searchResponse.bodyOrEmpty());
BigDecimal balanceAmount = balance != null ? balance.getBigDecimal("amount") : null;
String currencyCode = balance != null ? balance.getString("currencyCode") : null;
if (balanceAmount == null) {
return BookingCheckOutResult.failure(502, reservationKey, null, currencyCode,
"OHIP reservation search response does not contain roomStay balance amount",
searchSummary, null, null);
}
String paymentSummary = null;
if (balanceAmount.compareTo(BigDecimal.ZERO) > 0) {
JSONObject paymentBody = new JSONObject();
JSONObject criteria = new JSONObject();
JSONObject paymentMethod = new JSONObject();
paymentMethod.put("paymentMethod", "CA");
criteria.put("paymentMethod", paymentMethod);
JSONObject postingAmount = new JSONObject();
postingAmount.put("amount", balanceAmount);
postingAmount.put("currencyCode", currencyCode);
criteria.put("postingAmount", postingAmount);
criteria.put("cashierId", checkOutCashierId);
paymentBody.put("criteria", criteria);
HttpResponseResult paymentResponse;
try {
paymentResponse = executePost(
checkOutPaymentPathTemplate
.replace("{hotelId}", urlEncodePathSegment(ohipHotelId))
.replace("{reservationId}", urlEncodePathSegment(reservationKey)),
paymentBody,
ohipHotelId,
EsignAuditLogService.EVT_OHIP_POST_CHECK_OUT_PAYMENT,
"POST check out payment reservationId=" + reservationKey + " amount=" + balanceAmount);
} catch (IllegalStateException e) {
return BookingCheckOutResult.failure(502, reservationKey, balanceAmount, currencyCode,
e.getMessage(), searchSummary, null, null);
}
paymentSummary = truncate(paymentResponse.getResponseContent());
if (paymentResponse.isTransportError()) {
return BookingCheckOutResult.failure(502, reservationKey, balanceAmount, currencyCode,
StringUtils.defaultIfBlank(paymentResponse.getErrorMessage(), "OHIP payment transport error"),
searchSummary, paymentSummary, null);
}
if (!paymentResponse.isSuccessful()) {
return BookingCheckOutResult.failure(paymentResponse.getStatusCode(), reservationKey, balanceAmount, currencyCode,
extractOhipErrorMessage(paymentResponse.bodyOrEmpty(),
"OHIP payment returned HTTP " + paymentResponse.getStatusCode()),
searchSummary, paymentSummary, null);
}
}
JSONObject checkOutBody = new JSONObject();
JSONObject reservation = new JSONObject();
reservation.put("cashierId", checkOutCashierId);
reservation.put("autoCheckout", true);
checkOutBody.put("reservation", reservation);
HttpResponseResult checkOutResponse;
try {
checkOutResponse = executePost(
checkOutPathTemplate
.replace("{hotelId}", urlEncodePathSegment(ohipHotelId))
.replace("{reservationId}", urlEncodePathSegment(reservationKey)),
checkOutBody,
ohipHotelId,
EsignAuditLogService.EVT_OHIP_POST_CHECK_OUT,
"POST check out reservationId=" + reservationKey);
} catch (IllegalStateException e) {
return BookingCheckOutResult.failure(502, reservationKey, balanceAmount, currencyCode,
e.getMessage(), searchSummary, paymentSummary, null);
}
String checkOutSummary = truncate(checkOutResponse.getResponseContent());
if (checkOutResponse.isTransportError()) {
return BookingCheckOutResult.failure(502, reservationKey, balanceAmount, currencyCode,
StringUtils.defaultIfBlank(checkOutResponse.getErrorMessage(), "OHIP check out transport error"),
searchSummary, paymentSummary, checkOutSummary);
}
if (!checkOutResponse.isSuccessful()) {
return BookingCheckOutResult.failure(checkOutResponse.getStatusCode(), reservationKey, balanceAmount, currencyCode,
extractOhipErrorMessage(checkOutResponse.bodyOrEmpty(),
"OHIP check out returned HTTP " + checkOutResponse.getStatusCode()),
searchSummary, paymentSummary, checkOutSummary);
}
return BookingCheckOutResult.success(checkOutResponse.getStatusCode(), reservationKey, balanceAmount,
currencyCode, searchSummary, paymentSummary, checkOutSummary);
}
private HttpResponseResult executePost(
String path,
JSONObject body,
String hotelId,
String eventType,
String message) {
String url = SimpleHttpClient.joinUrl(baseUrl, path);
SimpleHttpClient.RequestBuilder request = SimpleHttpClient.post(url)
.timeoutMs(timeoutMs)
.jsonBody(body.toJSONString());
applyBusinessAuth(request, hotelId);
long start = System.currentTimeMillis();
HttpResponseResult response = request.execute();
auditThirdParty(eventType, request, response, start, hotelId, message);
return response;
}
private static JSONObject extractReservationBalance(String responseBody) {
JSONObject root = JSONObject.parseObject(responseBody);
JSONObject reservations = root.getJSONObject("reservations");
if (reservations == null) {
return null;
}
JSONArray reservationInfo = reservations.getJSONArray("reservationInfo");
if (reservationInfo == null || reservationInfo.isEmpty()) {
return null;
}
JSONObject reservation = reservationInfo.getJSONObject(0);
JSONObject roomStay = reservation != null ? reservation.getJSONObject("roomStay") : null;
return roomStay != null ? roomStay.getJSONObject("balance") : null;
}
public BookingStayExtensionResult extendStay(String hotelId, String reservationId, LocalDate departureDate) { public BookingStayExtensionResult extendStay(String hotelId, String reservationId, LocalDate departureDate) {
String reservationKey = StringUtils.trimToNull(reservationId); String reservationKey = StringUtils.trimToNull(reservationId);
String ohipHotelId = resolveOhipHotelId(hotelId); String ohipHotelId = resolveOhipHotelId(hotelId);
+8
View File
@@ -145,6 +145,14 @@ ops-app:
path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments
check-in: check-in:
path-template: /fof/v1/hotels/{hotelId}/reservations/{reservationId}/checkIns path-template: /fof/v1/hotels/{hotelId}/reservations/{reservationId}/checkIns
booking-check-out:
search:
path-template: /rsv/v1/hotels/{hotelId}/reservations/searches
payment:
path-template: /csh/v1/hotels/{hotelId}/reservations/{reservationId}/payments
check-out:
path-template: /csh/v1/hotels/{hotelId}/reservations/{reservationId}/checkOuts
cashier-id: 104
file-attachment: file-attachment:
path: /med/config/v1/fileAttachments path: /med/config/v1/fileAttachments
user-name: USER@HOTEL user-name: USER@HOTEL
+8
View File
@@ -142,6 +142,14 @@ ops-app:
path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments
check-in: check-in:
path-template: /fof/v1/hotels/{hotelId}/reservations/{reservationId}/checkIns path-template: /fof/v1/hotels/{hotelId}/reservations/{reservationId}/checkIns
booking-check-out:
search:
path-template: /rsv/v1/hotels/{hotelId}/reservations/searches
payment:
path-template: /csh/v1/hotels/{hotelId}/reservations/{reservationId}/payments
check-out:
path-template: /csh/v1/hotels/{hotelId}/reservations/{reservationId}/checkOuts
cashier-id: 104
file-attachment: file-attachment:
path: /med/config/v1/fileAttachments path: /med/config/v1/fileAttachments
user-name: USER@HOTEL user-name: USER@HOTEL
+8
View File
@@ -36,6 +36,14 @@ ops-app:
batch-max: 2000 batch-max: 2000
reservation: reservation:
third-party: third-party:
booking-check-out:
search:
path-template: /rsv/v1/hotels/{hotelId}/reservations/searches
payment:
path-template: /csh/v1/hotels/{hotelId}/reservations/{reservationId}/payments
check-out:
path-template: /csh/v1/hotels/{hotelId}/reservations/{reservationId}/checkOuts
cashier-id: 104
profile-fields: profile-fields:
enabled: true enabled: true
path-template: /crm/v1/profiles/{profileId} path-template: /crm/v1/profiles/{profileId}