保誠-保戶業務員媒合平台
wayne
2022-01-22 602c6a536fa1448e3f9f2d4c89b27f7aa0503295
[add] [todo 134462, 134463] 批次發送未處理預約單

修改3個檔案
新增5個檔案
266 ■■■■■ 已變更過的檔案
pamapi/src/main/java/com/pollex/pam/domain/AppointmentExpiringNotifyRecord.java 46 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentExpiringNotifyRecordRepository.java 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentRepository.java 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java 169 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/i18n/messages.properties 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/i18n/messages_zh_TW.properties 7 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/appointmentExpiringNotifyEmail.html 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/appointmentPendingNotifyEmail.html 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentExpiringNotifyRecord.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,46 @@
package com.pollex.pam.domain;
import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;
@Entity
@Table(name = "appointment_expiring_notify_record")
public class AppointmentExpiringNotifyRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "appointment_id")
    private Long appointmentId;
    @Column(name = "send_time")
    private Instant sendTime;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
    public Instant getSendTime() {
        return sendTime;
    }
    public void setSendTime(Instant sendTime) {
        this.sendTime = sendTime;
    }
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentExpiringNotifyRecordRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
package com.pollex.pam.repository;
import com.pollex.pam.domain.AppointmentExpiringNotifyRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface AppointmentExpiringNotifyRecordRepository extends JpaRepository<AppointmentExpiringNotifyRecord, Long> {
    List<AppointmentExpiringNotifyRecord> findAllByAppointmentId(Long appointmentId);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentRepository.java
@@ -3,6 +3,8 @@
import java.util.List;
import java.util.Optional;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -16,4 +18,6 @@
    List<Appointment> findByAgentNoAndCustomerId(String agentNo, Long customerId);
    Optional<Appointment> findTopByAgentNoAndCustomerIdOrderByAppointmentDateDesc(String agentNo, Long customerId);
    List<Appointment> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status);
}
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,169 @@
package com.pollex.pam.service;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentExpiringNotifyRecord;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.repository.AppointmentExpiringNotifyRecordRepository;
import com.pollex.pam.repository.AppointmentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class ScheduleTaskService {
    /**
     * é›»è©±åŠemail在建立預約單(T)後的N天視為未處理預約單
     * ç›®å‰N皆暫定為2
     */
    private static final int APPOINTMENT_PENDING_PHONE_INTERVAL = 2;
    private static final int APPOINTMENT_PENDING_EMAIL_INTERVAL = 2;
    /**
     * é›»è©±åŠemail在建立預約單(T)後的N天會被視為未處理預約單,當天批次會發送提醒給顧問
     * è€Œåœ¨å¾Œä¸€å¤©(T+N+1),就會發送批次給客戶告知 è©²é¡§å•å¯èƒ½å¿™ç¢Œç„¡æ³•處理,是否需要取消
     */
    private static final int APPOINTMENT_EXPIRING_PHONE_INTERVAL = APPOINTMENT_PENDING_PHONE_INTERVAL + 1;
    private static final int APPOINTMENT_EXPIRING_EMAIL_INTERVAL = APPOINTMENT_PENDING_EMAIL_INTERVAL + 1;
    /**
     * é€šçŸ¥å®¢æˆ¶çš„æ¬¡æ•¸é™åˆ¶
     */
    private static final int SEND_EXPIRING_NOTIFY_LIMIT = 1;
    private static final String NOT_CONTACTED_NOTIFY_SUBJECT = "預約單未進行聯繫通知";
    private static final Logger log = LoggerFactory.getLogger(ScheduleTaskService.class);
    @Autowired
    ConsultantService consultantService;
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    AppointmentRepository appointmentRepository;
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    ApplicationProperties applicationProperties;
    @Autowired
    AppointmentExpiringNotifyRecordRepository appointmentExpiringNotifyRecordRepository;
    @Scheduled(cron = "0 30 8 * * *")
    public void sendAppointmentPendingNotifyToConsultant() {
        log.info("Starting send appointment pending notify to consultant");
        Map<String, List<Appointment>> consultantWithPendingAppointments =
            appointmentRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AppointmentStatusEnum.AVAILABLE)
                .stream()
                .filter(appointment -> isAppointmentInInterval(appointment, APPOINTMENT_PENDING_PHONE_INTERVAL, APPOINTMENT_PENDING_EMAIL_INTERVAL))
                .collect(Collectors.groupingBy(Appointment::getAgentNo));
        consultantWithPendingAppointments.forEach((agentNo, pendingAppointments) -> {
            int pendingAppointmentsSum = pendingAppointments.size();
            Consultant consultant = consultantService.findByAgentNo(agentNo);
            String consultantPhoneNumber = consultant.getPhoneNumber();
            String consultantEmail = consultant.getEmail();
            String emailContent = getAppointmentPendingNotifyEmailContent(pendingAppointmentsSum);
            sendMsgService.sendMsgBySMS(consultantPhoneNumber, String.format("您有%s則預約單未進行聯繫,請盡速處理", pendingAppointmentsSum));
            sendMsgService.sendMsgByEmail(consultantEmail, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true);
        });
        log.info("Sending appointment pending notify to consultant finish");
    }
    @Scheduled(cron = "0 30 8 * * *")
    public void sendAppointmentExpiringNotifyToCustomer() {
        log.info("Starting send appointment expiring notify to customer");
        List<Appointment> allByCommunicateStatus =
            appointmentRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AppointmentStatusEnum.AVAILABLE)
                .stream()
                .filter(appointment -> isAppointmentInInterval(appointment, APPOINTMENT_EXPIRING_PHONE_INTERVAL, APPOINTMENT_EXPIRING_EMAIL_INTERVAL))
                .filter(this::isAppointmentNotifyNotOnLimit)
                .collect(Collectors.toList());
        allByCommunicateStatus.forEach(appointment -> {
            Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
            Optional<String> optionalPhone = Optional.ofNullable(appointment.getPhone()).filter(StringUtils::hasText);
            Optional<String> optionalEmail = Optional.ofNullable(appointment.getEmail()).filter(StringUtils::hasText);
            optionalPhone.ifPresent(phone ->
                sendMsgService.sendMsgBySMS(phone, String.format("很抱歉!您預約%s顧問正忙碌中,請您取消預約並改選其他顧問,請點擊網址:%s"
                    , consultant.getName(), getAppointmentUrl(appointment.getId())))
            );
            optionalEmail.ifPresent(email ->
                sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentUrl(appointment.getId())), true)
            );
            AppointmentExpiringNotifyRecord record = new AppointmentExpiringNotifyRecord();
            record.setAppointmentId(appointment.getId());
            record.setSendTime(Instant.now());
            appointmentExpiringNotifyRecordRepository.save(record);
        });
        log.info("Sending appointment expiring notify to customer finish");
    }
    private boolean isAppointmentInInterval(Appointment appointment, int phoneInterval, int emailInterval) {
        final boolean isHavePhone = StringUtils.hasText(appointment.getPhone());
        final boolean isHaveEmail = StringUtils.hasText(appointment.getEmail());
        LocalDate appointmentDate = appointment.getAppointmentDate().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate nowDate = Instant.now().atZone(ZoneId.systemDefault()).toLocalDate();
        long intervalDays = nowDate.toEpochDay() - appointmentDate.toEpochDay();
        final boolean isAppointmentExpiringByPhone = isHavePhone && intervalDays >= phoneInterval;
        final boolean isAppointmentExpiringByEmail = isHaveEmail && intervalDays >= emailInterval;
        return isAppointmentExpiringByPhone || isAppointmentExpiringByEmail;
    }
    private boolean isAppointmentNotifyNotOnLimit(Appointment appointment) {
        int sendNotifyToCustomerRecordSum =
            appointmentExpiringNotifyRecordRepository.findAllByAppointmentId(appointment.getId()).size();
        return sendNotifyToCustomerRecordSum < SEND_EXPIRING_NOTIFY_LIMIT;
    }
    private String getAppointmentUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "?notContactAppointmentId=" + appointmentId;
    }
    private String getAppointmentPendingNotifyEmailContent(int sum) {
        Context context = new Context();
        context.setVariable("pendingAppointmentSum", sum);
        return springTemplateEngine.process("mail/appointmentPendingNotifyEmail", context);
    }
    private String getAppointmentExpiringNotifyEmail(String consultantName, String notifyUrl) {
        Context context = new Context();
        context.setVariable("consultantName", consultantName);
        context.setVariable("notifyUrl", notifyUrl);
        return springTemplateEngine.process("mail/appointmentExpiringNotifyEmail", context);
    }
}
pamapi/src/main/resources/i18n/messages.properties
@@ -22,3 +22,9 @@
# satisfaction write email 
email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1}
# appointment pending notify email
email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406
# appointment expiring notify email
email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1}
pamapi/src/main/resources/i18n/messages_zh_TW.properties
@@ -22,3 +22,10 @@
# satisfaction write email 
email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1}
# appointment pending notify email
email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406
# appointment expiring notify email
email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1}
pamapi/src/main/resources/templates/mail/appointmentExpiringNotifyEmail.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
  <title>預約單未處理通知</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{email.write.appointment.expiring.content(${consultantName}, ${notifyUrl})}">很抱歉!您預約xxx顧問正忙碌中 , è«‹æ‚¨å–消預約並改選其他顧問</p>
</body>
</html>
pamapi/src/main/resources/templates/mail/appointmentPendingNotifyEmail.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
  <title>預約單未處理通知</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{email.write.appointment.pending.content(${pendingAppointmentSum})}">您有x則預約單未進行聯繫,請盡速處理</p>
</body>
</html>