From b5eac229cbabce7dc54cb5f53ab5faf8a416909d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E6=97=BB=E5=B3=B0?= <1441170195@qq.com> Date: Tue, 22 Sep 2026 10:42:59 +0800 Subject: [PATCH] feat:Add check out API --- .../audit/services/EsignAuditLogService.java | 3 + .../controllers/BookingCheckInController.java | 14 ++ .../services/ReservationThirdPartyClient.java | 176 ++++++++++++++++++ src/main/resources/application-dev.yml | 8 + src/main/resources/application-local.yml | 8 + src/main/resources/application-prod.yml | 8 + 6 files changed, 217 insertions(+) diff --git a/src/main/java/com/sjmresorts/opsapp/booking/audit/services/EsignAuditLogService.java b/src/main/java/com/sjmresorts/opsapp/booking/audit/services/EsignAuditLogService.java index 8e8cd98..84582c9 100644 --- a/src/main/java/com/sjmresorts/opsapp/booking/audit/services/EsignAuditLogService.java +++ b/src/main/java/com/sjmresorts/opsapp/booking/audit/services/EsignAuditLogService.java @@ -45,6 +45,9 @@ public class EsignAuditLogService { 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_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"; @Resource(name = "esignAuditLogRepository") diff --git a/src/main/java/com/sjmresorts/opsapp/booking/reservation/controllers/BookingCheckInController.java b/src/main/java/com/sjmresorts/opsapp/booking/reservation/controllers/BookingCheckInController.java index 359f151..5bcdfa0 100644 --- a/src/main/java/com/sjmresorts/opsapp/booking/reservation/controllers/BookingCheckInController.java +++ b/src/main/java/com/sjmresorts/opsapp/booking/reservation/controllers/BookingCheckInController.java @@ -2,6 +2,7 @@ package com.sjmresorts.opsapp.booking.reservation.controllers; import com.sjmresorts.opsapp.esign.common.EsignApiPaths; 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.BookingStayExtensionResult; import com.sjmresorts.opsapp.esign.reservation.services.ReservationThirdPartyClient; @@ -39,6 +40,19 @@ public class BookingCheckInController { return ResponseEntity.status(resolveHttpStatus(result.statusCode())).body(result); } + @PostMapping("/{reservationId}/check-out") + @Operation(summary = "办理 Booking 自动退房") + public ResponseEntity 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") @Operation(summary = "续房并修改 Booking 离店日期") public ResponseEntity extendStay( diff --git a/src/main/java/com/sjmresorts/opsapp/booking/reservation/services/ReservationThirdPartyClient.java b/src/main/java/com/sjmresorts/opsapp/booking/reservation/services/ReservationThirdPartyClient.java index dcf9a20..751c634 100644 --- a/src/main/java/com/sjmresorts/opsapp/booking/reservation/services/ReservationThirdPartyClient.java +++ b/src/main/java/com/sjmresorts/opsapp/booking/reservation/services/ReservationThirdPartyClient.java @@ -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.support.ThirdPartyAuditDetailBuilder; 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.ReservationThirdPartyResult; import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; @@ -25,6 +26,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.LocalDateTime; +import java.math.BigDecimal; import java.time.format.DateTimeFormatter; import java.util.Base64; 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}}") 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}") private String fileAttachmentPath; @@ -311,6 +325,168 @@ public class ReservationThirdPartyClient { 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) { String reservationKey = StringUtils.trimToNull(reservationId); String ohipHotelId = resolveOhipHotelId(hotelId); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3c64307..c07bdd3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -145,6 +145,14 @@ ops-app: path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments check-in: 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: path: /med/config/v1/fileAttachments user-name: USER@HOTEL diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index bf15843..4de8104 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -142,6 +142,14 @@ ops-app: path-template: /fof/v1/hotels/{hotelId}/reservations/autoRoomAssignments check-in: 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: path: /med/config/v1/fileAttachments user-name: USER@HOTEL diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 49897bc..758d697 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -36,6 +36,14 @@ ops-app: batch-max: 2000 reservation: 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: enabled: true path-template: /crm/v1/profiles/{profileId}