diff --git a/pom.xml b/pom.xml index 38b08b6..8393af6 100644 --- a/pom.xml +++ b/pom.xml @@ -120,6 +120,16 @@ spring-boot-starter-test test + + com.auth0 + java-jwt + 4.5.0 + + + com.eatthepath + pushy + 0.15.4 + diff --git a/src/main/java/com/mgm/mcrm/apigateway/common/constants/SysCommon.java b/src/main/java/com/mgm/mcrm/apigateway/common/constants/SysCommon.java index 1600bfb..1e06bbe 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/common/constants/SysCommon.java +++ b/src/main/java/com/mgm/mcrm/apigateway/common/constants/SysCommon.java @@ -9,7 +9,11 @@ import java.io.InputStreamReader; import java.util.*; +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.mgm.mcrm.apigateway.common.dtos.LoginUserInfo; import com.mgm.mcrm.apigateway.common.utils.LogUtil; +import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.system.ApplicationHome; import org.springframework.core.io.ClassPathResource; @@ -99,4 +103,27 @@ public class SysCommon { return result; } + public static LoginUserInfo parseUserFromToken(String tokenContent) { + LoginUserInfo loginUser = null; + if(StringUtils.isNotBlank(tokenContent)) { + try { + tokenContent = tokenContent.replace("Bearer ", ""); + + DecodedJWT decodeData = JWT.decode(tokenContent); + loginUser = new LoginUserInfo(); + String userId = decodeData.getSubject(); + if(StringUtils.isNotBlank(userId)) { + loginUser.setId(Long.parseLong(userId)); + }else { + loginUser.setId(-1L); + } + loginUser.setLoginId(decodeData.getClaim("login_id").asString()); + loginUser.setGroupName(decodeData.getClaim("user_group").asString()); + }catch(Exception err) { + LogUtil.error(SysCommon.class, "Parse Token failed:"+err.getMessage()); + loginUser =null; + } + } + return loginUser; + } } diff --git a/src/main/java/com/mgm/mcrm/apigateway/common/dtos/LoginUserInfo.java b/src/main/java/com/mgm/mcrm/apigateway/common/dtos/LoginUserInfo.java new file mode 100644 index 0000000..0cbae14 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/common/dtos/LoginUserInfo.java @@ -0,0 +1,12 @@ +package com.mgm.mcrm.apigateway.common.dtos; + +import lombok.Data; + +@Data +public class LoginUserInfo { + private Long id; + private String loginId; + private String displayName; + private String groupName; + private String tokenUuid; +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/common/utils/NotificationUtil.java b/src/main/java/com/mgm/mcrm/apigateway/common/utils/NotificationUtil.java new file mode 100644 index 0000000..97d28cc --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/common/utils/NotificationUtil.java @@ -0,0 +1,39 @@ +package com.mgm.mcrm.apigateway.common.utils; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +public class NotificationUtil { + + public String distinguishLanguage(String inputMsg,Integer languageIndex){ + + if (StringUtils.isBlank(inputMsg)) { + return inputMsg; + } + String preferredTag = Integer.valueOf(1).equals(languageIndex) ? "CN" : "EN"; + String fallbackTag = Integer.valueOf(1).equals(languageIndex) ? "EN" : "CN"; + String preferredText = extractNotificationText(inputMsg, preferredTag); + if (StringUtils.isNotBlank(preferredText)) { + return preferredText; + } + String fallbackText = extractNotificationText(inputMsg, fallbackTag); + return StringUtils.isNotBlank(fallbackText) ? fallbackText : inputMsg; + } + + + private static String extractNotificationText(String text, String tag) { + String marker = "#" + tag + "#"; + int start = text.indexOf(marker); + if (start < 0) { + return null; + } + start += marker.length(); + int end = text.indexOf(marker, start); + if (end < 0) { + return null; + } + return text.substring(start, end).trim(); + } + +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/daylight/common/scheduler/DaylightConfigurationScheduler.java b/src/main/java/com/mgm/mcrm/apigateway/daylight/common/scheduler/DaylightConfigurationScheduler.java index 7c3bc90..d5eea26 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/daylight/common/scheduler/DaylightConfigurationScheduler.java +++ b/src/main/java/com/mgm/mcrm/apigateway/daylight/common/scheduler/DaylightConfigurationScheduler.java @@ -46,6 +46,12 @@ public class DaylightConfigurationScheduler { public void fetchDaylightConfigurationOnStartupIfMissing() { LogUtil.info(DaylightConfigurationScheduler.class, "Checking saved Daylight configurations on startup"); try { + if (cfgDaylightConfigurationDao.existsSavedDaylightConfiguration()) { + LogUtil.info(DaylightConfigurationScheduler.class, "Saved Daylight configurations found, skip startup fetch"); + return; + } + + LogUtil.warn(DaylightConfigurationScheduler.class, "No saved Daylight configurations found, start startup fetch"); BaseApiResponse configResponse = daylightFetchConfigurationService.fetchDaylightConfiguration(); if (configResponse.isSuccess()) { LogUtil.info(DaylightConfigurationScheduler.class, "Startup configurations completed: " + configResponse.getMessage()); @@ -67,6 +73,12 @@ public class DaylightConfigurationScheduler { return; } + if (cfgDaylightConfigurationDao.existsSavedDaylightBlockCodes()) { + LogUtil.info(DaylightConfigurationScheduler.class, "Saved Daylight block codes found, skip startup fetch"); + return; + } + + LogUtil.warn(DaylightConfigurationScheduler.class, "No saved Daylight block codes found, start startup fetch"); BaseApiResponse blockResponse = daylightFetchConfigurationService.fetchDaylightBlockCodes(); if (blockResponse.isSuccess()) { LogUtil.info(DaylightConfigurationScheduler.class, "Startup block codes completed: " + blockResponse.getMessage()); diff --git a/src/main/java/com/mgm/mcrm/apigateway/daylight/common/services/DaylightFetchConfigurationService.java b/src/main/java/com/mgm/mcrm/apigateway/daylight/common/services/DaylightFetchConfigurationService.java index a49a672..a854f90 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/daylight/common/services/DaylightFetchConfigurationService.java +++ b/src/main/java/com/mgm/mcrm/apigateway/daylight/common/services/DaylightFetchConfigurationService.java @@ -106,7 +106,7 @@ public class DaylightFetchConfigurationService { String apiUrl = daylightApiSetting.getServerUrl() + path; String types = "PurchaseElement,MarketSegment,RatePlan,BookingSource,MembershipLevel," + "Attribute,RoomType,GuaranteeType,DistributionChannel,AgeBucket,CancellationReason," + - "ComplimentaryAccountingPreset,ComplimentaryType,TransactionCode"; + "ComplimentaryAccountingPreset,ComplimentaryType,TransactionCode,Room"; String pagingNext; int pageNumber = 1; diff --git a/src/main/java/com/mgm/mcrm/apigateway/daylight/repositories/CfgDaylightConfigurationDao.java b/src/main/java/com/mgm/mcrm/apigateway/daylight/repositories/CfgDaylightConfigurationDao.java index 69c07ae..4dbf457 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/daylight/repositories/CfgDaylightConfigurationDao.java +++ b/src/main/java/com/mgm/mcrm/apigateway/daylight/repositories/CfgDaylightConfigurationDao.java @@ -29,4 +29,11 @@ public interface CfgDaylightConfigurationDao extends JpaRepository ''") boolean existsSavedConfigurationByDlPropertyId(String dlPropertyId); + + @Query("select case when count(c) > 0 then true else false end " + + "from CfgDaylightConfiguration c " + + "where c.configName = 'BlockCode' " + + "and c.configValue is not null " + + "and c.configValue <> ''") + boolean existsSavedDaylightBlockCodes(); } diff --git a/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/controllers/DaylightWebhook.java b/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/controllers/DaylightWebhook.java index 6806c2b..cf6616c 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/controllers/DaylightWebhook.java +++ b/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/controllers/DaylightWebhook.java @@ -7,6 +7,7 @@ import com.mgm.mcrm.apigateway.logging.services.WebhookLogService; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.*; import java.util.Map; +@Slf4j @Tag(name = "Allotment", description = "APIs for Room Allotment functions") @RestController @RequestMapping("/api/daylight/webhook") @@ -56,6 +58,7 @@ public class DaylightWebhook { }catch(Exception err){ result.setSuccess(false); result.setMessage("Handle Message ["+messageId+"] failed with error:"+err.getMessage()); + log.error("Handle Message [{}] failed with error:", messageId, err); } } } diff --git a/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/eventhandlers/impl/ReservationNotificationHandler.java b/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/eventhandlers/impl/ReservationNotificationHandler.java index 8ac1f20..0ec0d32 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/eventhandlers/impl/ReservationNotificationHandler.java +++ b/src/main/java/com/mgm/mcrm/apigateway/daylight/webhook/eventhandlers/impl/ReservationNotificationHandler.java @@ -48,6 +48,7 @@ public class ReservationNotificationHandler implements DaylightWebhookHandler { private Bookings convertEventToBooking(ReservationNotificationEvent eventData) { + log.info("Reservation Notification Event Data: {}", JSON.toJSONString(eventData)); Bookings result = new Bookings(); diff --git a/src/main/java/com/mgm/mcrm/apigateway/logging/dao/SysLogDeviceTokenRepository.java b/src/main/java/com/mgm/mcrm/apigateway/logging/dao/SysLogDeviceTokenRepository.java new file mode 100644 index 0000000..6a030e9 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/logging/dao/SysLogDeviceTokenRepository.java @@ -0,0 +1,13 @@ +package com.mgm.mcrm.apigateway.logging.dao; + +import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SysLogDeviceTokenRepository extends JpaRepository { + + SysLogDeviceToken findByUserId(String bookerId); + + void deleteByUserId(String userId); +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/logging/models/SysLogDeviceToken.java b/src/main/java/com/mgm/mcrm/apigateway/logging/models/SysLogDeviceToken.java new file mode 100644 index 0000000..c782bc1 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/logging/models/SysLogDeviceToken.java @@ -0,0 +1,39 @@ +package com.mgm.mcrm.apigateway.logging.models; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.util.Date; + +@Data +@Entity +@Table(name = "sys_log_device_token", + indexes = { + @Index(name = "idx_device_token", columnList = "deviceToken") + }) +public class SysLogDeviceToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "user_id", unique = true, nullable = false, length = 64) + private String userId; + + @Column(name = "device_token", length = 255) + private String deviceToken; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private Date createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private Date updatedAt; + + @Column(name = "use_language_type") + private Integer useLanguageType; +} \ No newline at end of file diff --git a/src/main/java/com/mgm/mcrm/apigateway/logging/models/SysLogNotification.java b/src/main/java/com/mgm/mcrm/apigateway/logging/models/SysLogNotification.java new file mode 100644 index 0000000..56c532b --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/logging/models/SysLogNotification.java @@ -0,0 +1,66 @@ +package com.mgm.mcrm.apigateway.logging.models; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.Nationalized; + +import java.time.LocalDateTime; + +@Data +@Entity +@Table(name = "sys_log_notification") +public class SysLogNotification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "user_id", length = 64, nullable = false) + private String userId; + + @Column(name = "sender_id", length = 64) + private String senderId; + + @Column(name = "transaction_id") + private String transactionId; + + @Column(name = "device_token") + private String deviceToken; + + @Column(name = "status", nullable = false) + private Integer status; + + @Nationalized + @Column(name = "title", columnDefinition = "NVARCHAR(MAX)") + private String title; + + @Nationalized + @Column(name = "content", columnDefinition = "NVARCHAR(MAX)") + private String content; + + @Column(name = "booking_id", length = 100) + private String bookingId; + + @Column(name = "confirmation_no", length = 100) + private String confirmationNo; + + @Column(name = "booking_type") + private String bookingType; + + @Column(name = "result_message", length = 512) + private String resultMessage; + + @Column(name = "apn_response_id") + private String apnResponseId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @Column(name = "created_at") + private LocalDateTime createdAt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/logging/repositories/SysLogDeviceTokenDao.java b/src/main/java/com/mgm/mcrm/apigateway/logging/repositories/SysLogDeviceTokenDao.java new file mode 100644 index 0000000..7743197 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/logging/repositories/SysLogDeviceTokenDao.java @@ -0,0 +1,13 @@ +package com.mgm.mcrm.apigateway.logging.repositories; + +import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SysLogDeviceTokenDao extends JpaRepository { + + SysLogDeviceToken findByUserId(String bookerId); + + void deleteByUserId(String userId); +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/logging/repositories/SysLogNotificationRepository.java b/src/main/java/com/mgm/mcrm/apigateway/logging/repositories/SysLogNotificationRepository.java new file mode 100644 index 0000000..9cc46f5 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/logging/repositories/SysLogNotificationRepository.java @@ -0,0 +1,14 @@ +package com.mgm.mcrm.apigateway.logging.repositories; + +import com.mgm.mcrm.apigateway.logging.models.SysLogNotification; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SysLogNotificationRepository extends JpaRepository { + List findByUserIdAndStatus(String userId, Integer status); + + List findAllByDeviceTokenAndTransactionId(String deviceToken, String transactionId); + + long countByUserIdAndStatus(String userId, Integer status); +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/logging/services/SysLogDeviceTokenService.java b/src/main/java/com/mgm/mcrm/apigateway/logging/services/SysLogDeviceTokenService.java new file mode 100644 index 0000000..2459ef7 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/logging/services/SysLogDeviceTokenService.java @@ -0,0 +1,13 @@ +package com.mgm.mcrm.apigateway.logging.services; + +import com.mgm.mcrm.apigateway.common.utils.LogUtil; +import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken; +import com.mgm.mcrm.apigateway.logging.repositories.SysLogDeviceTokenDao; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +public class SysLogDeviceTokenService { +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/notification/services/ApnNotificationService.java b/src/main/java/com/mgm/mcrm/apigateway/notification/services/ApnNotificationService.java new file mode 100644 index 0000000..861e335 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/notification/services/ApnNotificationService.java @@ -0,0 +1,142 @@ +package com.mgm.mcrm.apigateway.notification.services; + +import com.eatthepath.pushy.apns.ApnsClient; +import com.eatthepath.pushy.apns.PushNotificationResponse; +import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder; +import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; +import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture; +import com.mgm.mcrm.apigateway.common.dtos.BaseApiResponse; +import com.mgm.mcrm.apigateway.common.utils.LogUtil; +import com.mgm.mcrm.apigateway.logging.models.SysLogNotification; +import com.mgm.mcrm.apigateway.logging.repositories.SysLogNotificationRepository; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class ApnNotificationService { + + @Autowired + private ApnsClient apnsClient; + @Value("${apns.bundle-id}") + private String bundleId; + + @Resource + private SysLogNotificationRepository notificationLogDao; + + private final Logger logger = LoggerFactory.getLogger(ApnNotificationService.class); + + public BaseApiResponse pushApnMessage(String transactionId, String deviceToken, String title, String body, boolean isWaitResponse) { + return pushApnMessage(transactionId, deviceToken, title, body, isWaitResponse, null); + } + + public BaseApiResponse pushApnMessage(String transactionId, String deviceToken, String title, String body, boolean isWaitResponse, String confirmNo) { + BaseApiResponse result = new BaseApiResponse(); + if(StringUtils.isBlank(deviceToken)){ + result.setSuccess(false); + result.setMessage("Device Token is empty"); + return result; + }else{ + LogUtil.info(ApnNotificationService.class, "Pushing Apn Start"); + SimpleApnsPayloadBuilder payloadBuilder = new SimpleApnsPayloadBuilder(); + payloadBuilder + .setAlertTitle(title) + .setAlertBody(body) + .setSound("default") + .setBadgeNumber(1); + if(StringUtils.isNotBlank(confirmNo)){ + payloadBuilder.addCustomProperty("ConfirmNo", confirmNo); + } + String payload = payloadBuilder.build(); + + SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification( + deviceToken, + bundleId, + payload + ); + PushNotificationFuture> + sendFuture = apnsClient.sendNotification(pushNotification); + if(isWaitResponse){ + String resultMessage =""; + boolean isSuccess =false; + String apnResponseId=null; + try { + PushNotificationResponse response = sendFuture.get(); + if(response!=null && response.isAccepted()){ + isSuccess=true; + apnResponseId = response.getApnsId()==null?null:response.getApnsId().toString(); + resultMessage = "APN message pushed success with response Id:"+response.getApnsId(); + }else if(response!=null && !response.isAccepted()){ + resultMessage = "APN message was rejected with reason:"+response.getRejectionReason(); + }else{ + resultMessage = "APN no response"; + } + }catch(Exception err){ + resultMessage = "Push APN message failed with error:"+err.getMessage(); + } + result.setSuccess(isSuccess); + result.setMessage(resultMessage); + LogUtil.info(ApnNotificationService.class, "Pushing Apn result:"+resultMessage); + if(StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(deviceToken)){ + List notificationLogs = notificationLogDao.findAllByDeviceTokenAndTransactionId(deviceToken, transactionId); + if(!notificationLogs.isEmpty()){ + LocalDateTime currentTime = LocalDateTime.now(); + for(SysLogNotification logItem:notificationLogs){ + logItem.setStatus(isSuccess?0:-1); + logItem.setApnResponseId(apnResponseId); + logItem.setUpdatedAt(currentTime); + logItem.setResultMessage(resultMessage); + } + notificationLogDao.saveAll(notificationLogs); + } + } + }else{ + result.setSuccess(true); + result.setMessage("APN message was pushed and process with ASYNC mode, use txnId to trace log"); + // process response in async mode + sendFuture.whenComplete((response, throwable) -> { + String resultMessage =""; + boolean isSuccess =false; + String apnResponseId=null; + if (response != null && response.isAccepted()) { + isSuccess =true; + apnResponseId = response.getApnsId()==null?null:response.getApnsId().toString(); + resultMessage = "APN message pushed success with response Id:"+response.getApnsId(); + } else if (response != null && !response.isAccepted()) { + resultMessage = "APN message was rejected with reason:"+response.getRejectionReason(); + } else { + if(throwable!=null){ + resultMessage = "Push APN message failed with error:"+throwable.getMessage(); + }else{ + resultMessage = "APN no response"; + } + } + if(!isSuccess) + logger.error(resultMessage); + //-- update notification log + if(StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(deviceToken)){ + List notificationLogs = notificationLogDao.findAllByDeviceTokenAndTransactionId(deviceToken, transactionId); + if(!notificationLogs.isEmpty()){ + LocalDateTime currentTime = LocalDateTime.now(); + for(SysLogNotification logItem:notificationLogs){ + logItem.setStatus(isSuccess?0:-1); + logItem.setApnResponseId(apnResponseId); + logItem.setUpdatedAt(currentTime); + logItem.setResultMessage(resultMessage); + } + notificationLogDao.saveAll(notificationLogs); + } + } + }); + } + } + return result; + } +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/proxy/config/ApnClientConfig.java b/src/main/java/com/mgm/mcrm/apigateway/proxy/config/ApnClientConfig.java new file mode 100644 index 0000000..f6966c0 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/proxy/config/ApnClientConfig.java @@ -0,0 +1,109 @@ +package com.mgm.mcrm.apigateway.proxy.config; + +import com.eatthepath.pushy.apns.ApnsClient; +import com.eatthepath.pushy.apns.ApnsClientBuilder; +import com.eatthepath.pushy.apns.auth.ApnsSigningKey; +import com.eatthepath.pushy.apns.proxy.HttpProxyHandlerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import java.io.InputStream; +import java.net.InetSocketAddress; + +@Configuration +public class ApnClientConfig { + + @Value("${apns.team-id}") + private String teamId; + + @Value("${apns.key-id}") + private String keyId; + + @Value("${apns.key-file}") + private String keyFilePath; + + @Value("${apns.host}") + private String apnHost; + + @Value("${apns.proxy.enabled:false}") + private boolean proxyEnabled; + + @Value("${apns.proxy.host:}") + private String proxyHost; + + @Value("${apns.proxy.port:0}") + private int proxyPort; + + @Value("${apns.proxy.username:}") + private String proxyUsername; + + @Value("${apns.proxy.password:}") + private String proxyPassword; + + @Bean + public ApnsClient apnsClient() throws Exception { + + String tid = teamId != null ? teamId.trim() : ""; + String kid = keyId != null ? keyId.trim() : ""; + + if (!StringUtils.hasText(tid) || !StringUtils.hasText(kid)) { + throw new IllegalStateException("apns.team-id / apns.key-id must be non-empty after trim"); + } + + InputStream privateKeyStream = openApnsKeyStream(keyFilePath); + if (privateKeyStream == null) { + throw new IllegalStateException( + "Cannot load APNs .p8 from classpath (apns.key-file=" + keyFilePath + "). " + + "Put file under src/main/resources and use path like /AuthKey_XXX.p8"); + } + + ApnsClientBuilder builder = new ApnsClientBuilder() + .setSigningKey(ApnsSigningKey.loadFromInputStream(privateKeyStream, tid, kid)) + .setApnsServer(apnHost.trim()); + + if (proxyEnabled) { + String host = proxyHost != null ? proxyHost.trim() : ""; + if (!StringUtils.hasText(host) || proxyPort <= 0) { + throw new IllegalStateException("apns.proxy.host / apns.proxy.port must be configured when apns.proxy.enabled=true"); + } + + InetSocketAddress proxyAddress = new InetSocketAddress(host, proxyPort); + if (StringUtils.hasText(proxyUsername)) { + builder.setProxyHandlerFactory(new HttpProxyHandlerFactory(proxyAddress, proxyUsername.trim(), proxyPassword != null ? proxyPassword : "")); + } else { + builder.setProxyHandlerFactory(new HttpProxyHandlerFactory(proxyAddress)); + } + } + + return builder.build(); + } + + + /** + * Spring Boot / fat jar 下用上下文 ClassLoader 读 resources 比 {@code getClass().getResourceAsStream} + *

+ * 更可靠;兼容 {@code /xxx.p8} 与 {@code xxx.p8} 两种写法。 + */ + + private static InputStream openApnsKeyStream(String configuredPath) { + if (!StringUtils.hasText(configuredPath)) { + return null; + } + + String path = configuredPath.trim(); + String withoutLeadingSlash = path.startsWith("/") ? path.substring(1) : path; + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + InputStream in = cl != null ? cl.getResourceAsStream(withoutLeadingSlash) : null; + + if (in != null) { + return in; + } + + return ApnClientConfig.class.getResourceAsStream(path.startsWith("/") ? path : "/" + path); + } +} + diff --git a/src/main/java/com/mgm/mcrm/apigateway/proxy/controllers/NotificationProxyController.java b/src/main/java/com/mgm/mcrm/apigateway/proxy/controllers/NotificationProxyController.java new file mode 100644 index 0000000..50bf4d0 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/proxy/controllers/NotificationProxyController.java @@ -0,0 +1,70 @@ +package com.mgm.mcrm.apigateway.proxy.controllers; + +import com.mgm.mcrm.apigateway.common.constants.SysCommon; +import com.mgm.mcrm.apigateway.common.dtos.BaseApiResponse; +import com.mgm.mcrm.apigateway.common.dtos.LoginUserInfo; +import com.mgm.mcrm.apigateway.common.dtos.config.DaylightApiSetting; +import com.mgm.mcrm.apigateway.common.dtos.config.EsbApiSetting; +import com.mgm.mcrm.apigateway.common.dtos.config.OperaApiSetting; +import com.mgm.mcrm.apigateway.daylight.common.services.DaylightTokenService; +import com.mgm.mcrm.apigateway.esb.common.services.EsbTokenService; +import com.mgm.mcrm.apigateway.proxy.dtos.request.SendNotificationRequest; +import com.mgm.mcrm.apigateway.proxy.exception.HttpProxyErrorHandler; +import com.mgm.mcrm.apigateway.proxy.exception.SoapProxyErrorHandler; +import com.mgm.mcrm.apigateway.proxy.services.NotificationProxyService; +import com.mgm.mcrm.apigateway.proxy.services.WebProxyService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.time.Duration; +import java.util.Collections; + +@Tag(name = "Notification Proxy", description = "Controller for Notification proxy") +@RestController +public class NotificationProxyController { + + private final Logger logger = LoggerFactory.getLogger(WebProxyController.class); + @Resource + private NotificationProxyService notificationProxyService; + + @RequestMapping("/api/notification/proxy/**") + public ResponseEntity invokeNotificationApiProxy(HttpServletRequest request, @RequestBody SendNotificationRequest notificationRequest) throws Exception { + LoginUserInfo requestUser = SysCommon.parseUserFromToken(request.getHeader("Authorization")); + return ResponseEntity.status(HttpStatus.OK).body(notificationProxyService.sendNotificationByRequest(notificationRequest,requestUser.getLoginId())); + } + + private HttpHeaders extractHeaders(HttpServletRequest request) { + HttpHeaders headers = new HttpHeaders(); + Collections.list(request.getHeaderNames()).forEach(name -> { + if (!name.equalsIgnoreCase("host") && + !name.equalsIgnoreCase("content-length") && + !name.equalsIgnoreCase("application-id") && + !name.equalsIgnoreCase("authorization") + ) { + headers.add(name, request.getHeader(name)); + } + }); + return headers; + } + + private byte[] getRequestBody(HttpServletRequest request) { + try { + return request.getInputStream().readAllBytes(); + } catch (IOException e) { + logger.error("Failed to read request body of request[{}]",request.getRequestURI(), e); + return new byte[0]; + } + } +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/proxy/dtos/request/SendNotificationRequest.java b/src/main/java/com/mgm/mcrm/apigateway/proxy/dtos/request/SendNotificationRequest.java new file mode 100644 index 0000000..59b27d6 --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/proxy/dtos/request/SendNotificationRequest.java @@ -0,0 +1,29 @@ +package com.mgm.mcrm.apigateway.proxy.dtos.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "Request to send notification by employeeId") +public class SendNotificationRequest { + @Schema(description = "Transaction Id for external tracing, if blank, internal uuid will be generated") + private String transactionId; + + @Schema(description = "Employee Id to receive notification") + @NotBlank(message = "Employee Id must not be empty") + private String employeeId; + + @Schema(description = "Title of message") + @NotBlank(message = "Title of message must not be blank") + private String title; + + @Schema(description = "Content of message") + private String content; + + @Schema(description = "Booking confirmation No if need") + private String confirmationNumber; + + @Schema(description = "Type of booking ") + private String type; +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/proxy/services/NotificationProxyService.java b/src/main/java/com/mgm/mcrm/apigateway/proxy/services/NotificationProxyService.java new file mode 100644 index 0000000..b9c0aaf --- /dev/null +++ b/src/main/java/com/mgm/mcrm/apigateway/proxy/services/NotificationProxyService.java @@ -0,0 +1,132 @@ +package com.mgm.mcrm.apigateway.proxy.services; + + +import com.mgm.mcrm.apigateway.common.dtos.BaseApiResponse; +import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken; +import com.mgm.mcrm.apigateway.logging.models.SysLogNotification; +import com.mgm.mcrm.apigateway.logging.repositories.SysLogDeviceTokenDao; +import com.mgm.mcrm.apigateway.logging.repositories.SysLogNotificationRepository; +import com.mgm.mcrm.apigateway.notification.services.ApnNotificationService; +import com.mgm.mcrm.apigateway.proxy.dtos.request.SendNotificationRequest; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class NotificationProxyService { + + @Resource + private SysLogDeviceTokenDao sysLogDeviceTokenDao; + + @Resource + private SysLogNotificationRepository sysLogNotificationRepository; + + @Resource + private ApnNotificationService apnNotificationService; + + public BaseApiResponse sendNotificationByRequest(SendNotificationRequest requestData, String senderId) { + BaseApiResponse result = new BaseApiResponse(); + String[] userIds = requestData.getEmployeeId().split("[;,]"); + String transactionId = requestData.getTransactionId(); + if(StringUtils.isBlank(transactionId)){ + transactionId = UUID.randomUUID().toString(); + } +// result.setData(transactionId); + if(userIds.length>0){ + int successCount =0; + for(String userId:userIds){ + if(StringUtils.isNotBlank(userId)){ + userId = userId.trim(); + SysLogDeviceToken deviceToken = sysLogDeviceTokenDao.findByUserId(userId); + String deviceTokenValue =null; + if(deviceToken!=null){ + deviceTokenValue = deviceToken.getDeviceToken(); + } + + Integer useLanguage = deviceToken == null ? null : deviceToken.getUseLanguageType(); + String notificationTitle = resolveNotificationText(requestData.getTitle(), useLanguage); + String notificationContent = resolveNotificationText(requestData.getContent(), useLanguage); + + //这里需要加上判断,判断deviceToken中的UseLanguage是什么语言 0:英文 1:中文 根据不同的Language来推送不同语言的通知 + + SysLogNotification notificationLog = createNotificationLogByRequest(requestData,transactionId,senderId,userId,deviceTokenValue,notificationTitle,notificationContent); + if(StringUtils.isBlank(deviceTokenValue)){ + notificationLog.setStatus(-1); + notificationLog.setResultMessage("No APN token is setup for user:"+userId); + notificationLog.setUpdatedAt(LocalDateTime.now()); + } + sysLogNotificationRepository.save(notificationLog); + //-- Process APN sending + BaseApiResponse sendResult = apnNotificationService.pushApnMessage( + transactionId, + deviceTokenValue, + notificationTitle, + notificationContent, + true, + requestData.getConfirmationNumber()); + if(sendResult.isSuccess()){ + successCount++; + } + } + } + if(successCount>0){ + result.setSuccess(true); + }else{ + result.setSuccess(false); + result.setMessage("No message was success processed"); + } + }else{ + result.setSuccess(false); + result.setMessage("employeeId list is empty"); + } + return result; + } + + private static String resolveNotificationText(String text, Integer useLanguage) { + if (StringUtils.isBlank(text)) { + return text; + } + String preferredTag = Integer.valueOf(1).equals(useLanguage) ? "CN" : "EN"; + String fallbackTag = Integer.valueOf(1).equals(useLanguage) ? "EN" : "CN"; + String preferredText = extractNotificationText(text, preferredTag); + if (StringUtils.isNotBlank(preferredText)) { + return preferredText; + } + String fallbackText = extractNotificationText(text, fallbackTag); + return StringUtils.isNotBlank(fallbackText) ? fallbackText : text; + } + + private static String extractNotificationText(String text, String tag) { + String marker = "#" + tag + "#"; + int start = text.indexOf(marker); + if (start < 0) { + return null; + } + start += marker.length(); + int end = text.indexOf(marker, start); + if (end < 0) { + return null; + } + return text.substring(start, end).trim(); + } + + private SysLogNotification createNotificationLogByRequest(SendNotificationRequest requestData,String transactionId, String senderId, String userId, String deviceToken, String title, String content){ + SysLogNotification notificationLog = new SysLogNotification(); + notificationLog.setUserId(userId); + notificationLog.setDeviceToken(deviceToken); + notificationLog.setSenderId(senderId); + notificationLog.setStatus(0); + notificationLog.setConfirmationNo(requestData.getConfirmationNumber()); + notificationLog.setBookingType(requestData.getType()); + notificationLog.setTitle(title); + notificationLog.setContent(content); + notificationLog.setTransactionId(transactionId); + notificationLog.setCreatedAt(LocalDateTime.now()); + return notificationLog; + } +} diff --git a/src/main/java/com/mgm/mcrm/apigateway/reservation/services/BookingService.java b/src/main/java/com/mgm/mcrm/apigateway/reservation/services/BookingService.java index 5f63aab..ee72804 100644 --- a/src/main/java/com/mgm/mcrm/apigateway/reservation/services/BookingService.java +++ b/src/main/java/com/mgm/mcrm/apigateway/reservation/services/BookingService.java @@ -22,19 +22,26 @@ import com.mgm.mcrm.apigateway.basesetting.room.repositories.RoomTypeDao; import com.mgm.mcrm.apigateway.common.dtos.ServiceResult; import com.mgm.mcrm.apigateway.common.utils.DateTimeUtil; import com.mgm.mcrm.apigateway.common.utils.LogUtil; +import com.mgm.mcrm.apigateway.common.utils.NotificationUtil; import com.mgm.mcrm.apigateway.esb.common.dtos.mgmapi.PlayerProfileApiResult; import com.mgm.mcrm.apigateway.esb.common.dtos.mgmapi.UserInfoApiRecord; import com.mgm.mcrm.apigateway.esb.player.services.MgmUserService; +import com.mgm.mcrm.apigateway.logging.dao.SysLogDeviceTokenRepository; +import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken; +import com.mgm.mcrm.apigateway.notification.services.ApnNotificationService; import com.mgm.mcrm.apigateway.player.services.PlayerService; import com.mgm.mcrm.apigateway.reservation.models.Bookings; import com.mgm.mcrm.apigateway.reservation.repositories.BookingDao; import jakarta.annotation.Resource; +import lombok.extern.java.Log; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -65,11 +72,20 @@ public class BookingService { private MgmUserService mgmUserService; @Resource private BlockCodeDao blockCodeDao; + @Resource + private SysLogDeviceTokenRepository sysLogDeviceTokenRepository; + @Resource + private NotificationUtil notificationUtil; + @Resource + private ApnNotificationService apnNotificationService; public void processDaylightUpdateBooking(Bookings daylightBooking) throws Exception { if(daylightBooking!=null){ String dlReservationId=daylightBooking.getDlReservationId(); String confirmNo = daylightBooking.getConfirmationNo(); + String dbBookingStatus = null; + LocalDateTime checkInDate = null; + LocalDateTime checkOutDate = null; if(StringUtils.isNotBlank(confirmNo) && StringUtils.isNotBlank(dlReservationId) ){ @@ -92,6 +108,9 @@ public class BookingService { for(Bookings booking:dbBookings){ if("N".equalsIgnoreCase(booking.getApproveStatus())) { dbBooking = booking; + dbBookingStatus = dbBooking.getLastOperaStatus(); + checkInDate = dbBooking.getCheckinDate(); + checkOutDate = dbBooking.getCheckoutDate(); break; } } @@ -102,6 +121,88 @@ public class BookingService { dbBooking = this.createBookingFromDaylight(daylightBooking); } bookingDao.save(dbBooking); + + CfgProperties property = propertyDao.findByDlPropertyId(dbBooking.getDlPropertyId()); + + //Send Notification to Booker when booking lastOpera status Changed + + LogUtil.info(BookingService.class,"Daylight webhook updated, prepare send notification"); + LogUtil.info(BookingService.class,"DaylightBooking status=" + daylightBooking.getLastOperaStatus()); + LogUtil.info(BookingService.class,"Db booking status=" + dbBookingStatus); + if (Strings.CS.equals(daylightBooking.getLastOperaStatus(),"RESERVED") && !Strings.CS.equals(dbBookingStatus,daylightBooking.getLastOperaStatus())){ + LogUtil.info(BookingService.class,"Booking last opera status changed, DB Record status:"+dbBookingStatus+", DlBooking status:"+daylightBooking.getLastOperaStatus()); + //设置Notification 内容 + String notificationTitle = "#EN# Room status updated#EN#" + + "#CN#房間狀態已更改#CN#"; + String notificationContent = "#EN#Player {playerId}, {propertyName}, {roomCode},room status changed to reserved.#EN#" + + "#CN#Player {playerId}, {propertyName}, {roomCode}, 房間狀態已預訂。#CN#"; + String[] placeholders = { + "{playerId}", "{propertyName}", "{roomCode}" + }; + LogUtil.info(BookingService.class,"Set notification info"); + String[] values = { + StringUtils.defaultString(dbBooking.getPlayerId()), + StringUtils.defaultString(property == null ? null : property.getName()), + StringUtils.defaultString(dbBooking.getRoomTypeCode()) + }; + LogUtil.info(BookingService.class,"Replace each"); + notificationContent = StringUtils.replaceEach(notificationContent, placeholders, values); + String transactionId = "DLBKSC"+dbBooking.getId(); + LogUtil.info(BookingService.class,"Get device token from DB Start"); + SysLogDeviceToken deviceToken = sysLogDeviceTokenRepository.findByUserId(StringUtils.trim(dbBooking.getCreatedBy())); + LogUtil.info(BookingService.class,"Get device token from DB:" + (deviceToken == null ? "null" : deviceToken.getDeviceToken())); + String deviceTokenValue =null; + String finalTitle = null; + String finalContent = null; + if(deviceToken!=null){ + deviceTokenValue = deviceToken.getDeviceToken(); + finalTitle = notificationUtil.distinguishLanguage(notificationTitle,deviceToken.getUseLanguageType()); + notificationContent = notificationUtil.distinguishLanguage(notificationContent,deviceToken.getUseLanguageType()); + + } +// NOTIFICATION + apnNotificationService.pushApnMessage(transactionId,deviceTokenValue,finalTitle,notificationContent,true); + } + + //Send notification to Booker when Booking C/I | C/O date changed + if ((checkInDate != null && !checkInDate.equals(daylightBooking.getCheckinDate()))|| + (checkOutDate != null && !checkOutDate.equals(daylightBooking.getCheckoutDate()))) { + LogUtil.info(BookingService.class,"Booking date changed, DB Record C/I:"+checkInDate+", DB Record C/O:"+checkOutDate+ + ", Dl Record C/I"+daylightBooking.getCheckinDate()+", Dl Record C/O"+daylightBooking.getCheckoutDate()); + + + //设置Notification 内容 + String notificationTitle = "#EN# Room status updated#EN#" + + "#CN#房間狀態已更改#CN#"; + String notificationContent = "#EN#Player {playerId}, {propertyName}, {roomCode},Stay date changed.#EN#" + + "#CN#Player {playerId}, {propertyName}, {roomCode}, 住宿日期已變更。#CN#"; + String[] placeholders = { + "{playerId}", "{propertyName}", "{roomCode}" + }; + String[] values = { + StringUtils.defaultString(dbBooking.getPlayerId()), + StringUtils.defaultString(property == null ? null : property.getName()), + StringUtils.defaultString(dbBooking.getRoomTypeCode()) + }; + + notificationContent = StringUtils.replaceEach(notificationContent, placeholders, values); + String transactionId = "DLBKDC" + dbBooking.getId(); + + SysLogDeviceToken deviceToken = sysLogDeviceTokenRepository.findByUserId(StringUtils.trim(dbBooking.getCreatedBy())); + LogUtil.info(BookingService.class,"Get device token from DB:" + (deviceToken == null ? "null" : deviceToken.getDeviceToken())); + String deviceTokenValue = null; + String finalTitle = null; + String finalContent = null; + if (deviceToken != null) { + LogUtil.info(BookingService.class,"Get device Token success"); + deviceTokenValue = deviceToken.getDeviceToken(); + finalTitle = notificationUtil.distinguishLanguage(notificationTitle, deviceToken.getUseLanguageType()); + notificationContent = notificationUtil.distinguishLanguage(notificationContent, deviceToken.getUseLanguageType()); + } +// NOTIFICATION + apnNotificationService.pushApnMessage(transactionId, deviceTokenValue, finalTitle, notificationContent, true); + } + LogUtil.info(BookingService.class,"End send notification function"); } } } @@ -128,6 +229,14 @@ public class BookingService { updateBooking.setDlAccountId(daylightBooking.getDlAccountId()); updateBooking.setDlBlockId(daylightBooking.getDlBlockId()); updateBooking.setDlExternalNo(daylightBooking.getDlExternalNo()); + + if (updateBooking.getWaitlistId() != null) { + bookingDao.findById(updateBooking.getWaitlistId()).ifPresent(booking -> { + booking.setConfirmationNo(daylightBooking.getConfirmationNo()); + bookingDao.save(booking); + }); + } + // updateBooking.setGroupId(daylightBooking.getGroupId()); updateBooking.setDlReservationId(daylightBooking.getDlReservationId()); // 通过 dlRoomTypeId 查找 room type(与 createBookingFromDaylight 流程保持一致) @@ -190,7 +299,7 @@ public class BookingService { String booker = StringUtils.trim(updateBooking.getBooker()); if(booker==null||StringUtils.isEmpty(booker)) getUser=true; if(daylightBooking.getCreatedBy()!=null && !daylightBooking.getCreatedBy().equals(updateBooking.getCreatedBy())) { - updateBooking.setCreatedBy(daylightBooking.getCreatedBy()); +// updateBooking.setCreatedBy(daylightBooking.getCreatedBy()); getUser=true; } if(getUser) { diff --git a/src/main/resources/AuthKey_79FNU7K4J9_Development.p8 b/src/main/resources/AuthKey_79FNU7K4J9_Development.p8 new file mode 100644 index 0000000..08d3b84 --- /dev/null +++ b/src/main/resources/AuthKey_79FNU7K4J9_Development.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgfQ1bQlwCYsPyrRua +2OB2VRxxlA2/3PUFWuS/+/3P1RygCgYIKoZIzj0DAQehRANCAASn2hmu3BJ02OmZ +hNS/y6SwiIJi4tWQB1lxX0EksfsRgU1e3pp9gcMeIvY8gQA44QQJSvV7JWIu2fMj +1b9xpZU/ +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 1a518b3..3b3454e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -137,4 +137,20 @@ springdoc: api-docs: path: /v3/api-docs default-consumes-media-type: application/json - default-produces-media-type: application/json \ No newline at end of file + default-produces-media-type: application/json + +apns: + team-id: YXJ2H36G5R + key-id: 79FNU7K4J9 + bundle-id: com.mgm.crmhostroombookingdaylightdevelopment + key-file: /AuthKey_79FNU7K4J9_Development.p8 + proxy: + enabled: false + host: + port: 0 + username: + password: + # Dev host + host: api.sandbox.push.apple.com + # Production Host +# host: api.push.apple.com \ No newline at end of file