blob: ba3e487a4125312e3c50b12c952b9b4bfd71c26f [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.campaigns.email.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.google.gson.Gson;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignValidator;
import org.apache.fineract.infrastructure.campaigns.email.data.PreviewCampaignMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaign;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignMustBeClosedToBeDeletedException;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignMustBeClosedToEditException;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignNotFound;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
import org.apache.fineract.infrastructure.dataqueries.domain.Report;
import org.apache.fineract.infrastructure.dataqueries.domain.ReportParameterUsage;
import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampaignWritePlatformService {
private final PlatformSecurityContext context;
private final EmailCampaignRepository emailCampaignRepository;
private final EmailCampaignValidator emailCampaignValidator;
private final ReportRepository reportRepository;
private final EmailMessageRepository emailMessageRepository;
private final ClientRepositoryWrapper clientRepositoryWrapper;
private final ReadReportingService readReportingService;
private final GenericDataService genericDataService;
private final FromJsonHelper fromJsonHelper;
@Transactional
@Override
public CommandProcessingResult create(JsonCommand command) {
final AppUser currentUser = this.context.authenticatedUser();
this.emailCampaignValidator.validateCreate(command.json());
final Long businessRuleId = command.longValueOfParameterNamed(EmailCampaignValidator.businessRuleId);
final Report businessRule = this.reportRepository.findById(businessRuleId)
.orElseThrow(() -> new ReportNotFoundException(businessRuleId));
final Long reportId = command.longValueOfParameterNamed(EmailCampaignValidator.stretchyReportId);
Report report = null;
Map<String, String> stretchyReportParams = null;
if (reportId != null) {
report = this.reportRepository.findById(reportId).orElseThrow(() -> new ReportNotFoundException(reportId));
final Set<ReportParameterUsage> reportParameterUsages = report.getReportParameterUsages();
stretchyReportParams = new HashMap<>();
if (reportParameterUsages != null && !reportParameterUsages.isEmpty()) {
for (final ReportParameterUsage reportParameterUsage : reportParameterUsages) {
stretchyReportParams.put(reportParameterUsage.getReportParameterName(), "");
}
}
}
EmailCampaign emailCampaign = EmailCampaign.instance(currentUser, businessRule, report, command);
if (stretchyReportParams != null) {
emailCampaign.setStretchyReportParamMap(new Gson().toJson(stretchyReportParams));
}
this.emailCampaignRepository.saveAndFlush(emailCampaign);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(emailCampaign.getId()).build();
}
@Transactional
@Override
public CommandProcessingResult update(final Long resourceId, final JsonCommand command) {
try {
this.context.authenticatedUser();
this.emailCampaignValidator.validateForUpdate(command.json());
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(resourceId)
.orElseThrow(() -> new EmailCampaignNotFound(resourceId));
if (emailCampaign.isActive()) {
throw new EmailCampaignMustBeClosedToEditException(emailCampaign.getId());
}
final Map<String, Object> changes = emailCampaign.update(command);
if (changes.containsKey(EmailCampaignValidator.businessRuleId)) {
final Long newValue = command.longValueOfParameterNamed(EmailCampaignValidator.businessRuleId);
final Report reportId = this.reportRepository.findById(newValue).orElseThrow(() -> new ReportNotFoundException(newValue));
emailCampaign.setBusinessRuleId(reportId);
}
if (!changes.isEmpty()) {
this.emailCampaignRepository.saveAndFlush(emailCampaign);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(resourceId).with(changes).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
@Transactional
@Override
public CommandProcessingResult delete(final Long resourceId) {
this.context.authenticatedUser();
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(resourceId)
.orElseThrow(() -> new EmailCampaignNotFound(resourceId));
if (emailCampaign.isActive()) {
throw new EmailCampaignMustBeClosedToBeDeletedException(emailCampaign.getId());
}
/*
* Do not delete but set a boolean is_visible to zero
*/
emailCampaign.delete();
this.emailCampaignRepository.saveAndFlush(emailCampaign);
return new CommandProcessingResultBuilder() //
.withEntityId(emailCampaign.getId()) //
.build();
}
@Override
public void insertDirectCampaignIntoEmailOutboundTable(final Loan loan, final EmailCampaign emailCampaign,
HashMap<String, String> campaignParams) {
try {
List<HashMap<String, Object>> runReportObject = this.getRunReportByServiceImpl(campaignParams.get("reportName"),
campaignParams);
if (runReportObject != null) {
for (HashMap<String, Object> entry : runReportObject) {
String message = this.compileEmailTemplate(emailCampaign.getEmailMessage(), emailCampaign.getCampaignName(), entry);
Client client = loan.getClient();
String emailAddress = client.emailAddress();
if (emailAddress != null && isValidEmail(emailAddress)) {
EmailMessage emailMessage = EmailMessage.pendingEmail(null, client, null, emailCampaign,
emailCampaign.getEmailSubject(), message, emailAddress, emailCampaign.getCampaignName());
this.emailMessageRepository.save(emailMessage);
}
}
}
} catch (final IOException e) {
// TODO throw something here
}
}
private void insertDirectCampaignIntoEmailOutboundTable(final String emailParams, final String emailSubject,
final String messageTemplate, final String campaignName, final Long campaignId) {
try {
HashMap<String, String> campaignParams = new ObjectMapper().readValue(emailParams,
new TypeReference<HashMap<String, String>>() {});
HashMap<String, String> queryParamForRunReport = new ObjectMapper().readValue(emailParams,
new TypeReference<HashMap<String, String>>() {});
List<HashMap<String, Object>> runReportObject = this.getRunReportByServiceImpl(campaignParams.get("reportName"),
queryParamForRunReport);
if (runReportObject != null) {
for (HashMap<String, Object> entry : runReportObject) {
String message = this.compileEmailTemplate(messageTemplate, campaignName, entry);
Integer clientId = (Integer) entry.get("id");
EmailCampaign emailCampaign = this.emailCampaignRepository.findById(campaignId).orElse(null);
Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue());
String emailAddress = client.emailAddress();
if (emailAddress != null && isValidEmail(emailAddress)) {
EmailMessage emailMessage = EmailMessage.pendingEmail(null, client, null, emailCampaign, emailSubject, message,
emailAddress, campaignName);
this.emailMessageRepository.save(emailMessage);
}
}
}
} catch (final IOException e) {
// TODO throw something here
}
}
public static boolean isValidEmail(String email) {
boolean isValid = true;
try {
InternetAddress emailO = new InternetAddress(email);
emailO.validate();
} catch (AddressException ex) {
isValid = false;
}
return isValid;
}
@Transactional
@Override
public CommandProcessingResult activateEmailCampaign(Long campaignId, JsonCommand command) {
final AppUser currentUser = this.context.authenticatedUser();
this.emailCampaignValidator.validateActivation(command.json());
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(campaignId)
.orElseThrow(() -> new EmailCampaignNotFound(campaignId));
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate");
emailCampaign.activate(currentUser, fmt, activationDate);
this.emailCampaignRepository.saveAndFlush(emailCampaign);
if (emailCampaign.isDirect()) {
insertDirectCampaignIntoEmailOutboundTable(emailCampaign.getParamValue(), emailCampaign.getEmailSubject(),
emailCampaign.getEmailMessage(), emailCampaign.getCampaignName(), emailCampaign.getId());
} else if (emailCampaign.isSchedule()) {
// if recurrence start date is in the past, calculate next trigger date, otherwise use recurrence start
// date as next trigger date when activating
LocalDateTime nextTriggerDateWithTime;
LocalDateTime recurrenceStartDate = emailCampaign.getRecurrenceStartDate();
LocalDateTime tenantDateTime = DateUtils.getLocalDateTimeOfTenant();
if (DateUtils.isBefore(recurrenceStartDate, tenantDateTime)) {
nextTriggerDateWithTime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), recurrenceStartDate,
tenantDateTime);
} else {
nextTriggerDateWithTime = recurrenceStartDate;
}
emailCampaign.setNextTriggerDate(nextTriggerDateWithTime);
this.emailCampaignRepository.saveAndFlush(emailCampaign);
}
/*
* if campaign is direct insert campaign message into email outbound table else if its a schedule create a job
* process for it
*/
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(emailCampaign.getId()) //
.build();
}
@Transactional
@Override
public CommandProcessingResult closeEmailCampaign(Long campaignId, JsonCommand command) {
final AppUser currentUser = this.context.authenticatedUser();
this.emailCampaignValidator.validateClosedDate(command.json());
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(campaignId)
.orElseThrow(() -> new EmailCampaignNotFound(campaignId));
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final LocalDate closureDate = command.localDateValueOfParameterNamed("closureDate");
emailCampaign.close(currentUser, fmt, closureDate);
this.emailCampaignRepository.saveAndFlush(emailCampaign);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(emailCampaign.getId()) //
.build();
}
private String compileEmailTemplate(final String textMessageTemplate, final String campaignName,
final Map<String, Object> emailParams) {
final MustacheFactory mf = new DefaultMustacheFactory();
final Mustache mustache = mf.compile(new StringReader(textMessageTemplate), campaignName);
final StringWriter stringWriter = new StringWriter();
mustache.execute(stringWriter, emailParams);
return stringWriter.toString();
}
@SuppressWarnings({ "unused", "rawtypes" })
@Override
public List<HashMap<String, Object>> getRunReportByServiceImpl(final String reportName, final Map<String, String> queryParams)
throws IOException {
final String reportType = "report";
List<HashMap<String, Object>> resultList;
final GenericResultsetData results = this.readReportingService.retrieveGenericResultSetForSmsEmailCampaign(reportName, reportType,
queryParams);
final String response = this.genericDataService.generateJsonFromGenericResultsetData(results);
resultList = new ObjectMapper().readValue(response, new TypeReference<>() {});
// loop changes array date to string date
for (Iterator<HashMap<String, Object>> it = resultList.iterator(); it.hasNext();) {
HashMap<String, Object> entry = it.next();
for (Iterator<Map.Entry<String, Object>> iter = entry.entrySet().iterator(); iter.hasNext();) {
Map.Entry<String, Object> map = iter.next();
String key = map.getKey();
Object ob = map.getValue();
if (ob instanceof ArrayList && ((ArrayList) ob).size() == 3) {
String changeArrayDateToStringDate = ((ArrayList) ob).get(2).toString() + "-" + ((ArrayList) ob).get(1).toString() + "-"
+ ((ArrayList) ob).get(0).toString();
entry.put(key, changeArrayDateToStringDate);
}
}
}
return resultList;
}
@Override
public PreviewCampaignMessage previewMessage(final JsonQuery query) {
PreviewCampaignMessage campaignMessage = null;
this.context.authenticatedUser();
this.emailCampaignValidator.validatePreviewMessage(query.json());
final String emailParams = this.fromJsonHelper.extractStringNamed("paramValue", query.parsedJson());
final String textMessageTemplate = this.fromJsonHelper.extractStringNamed("emailMessage", query.parsedJson());
try {
HashMap<String, String> campaignParams = new ObjectMapper().readValue(emailParams,
new TypeReference<HashMap<String, String>>() {});
HashMap<String, String> queryParamForRunReport = new ObjectMapper().readValue(emailParams,
new TypeReference<HashMap<String, String>>() {});
List<HashMap<String, Object>> runReportObject = this.getRunReportByServiceImpl(campaignParams.get("reportName"),
queryParamForRunReport);
if (runReportObject != null) {
for (HashMap<String, Object> entry : runReportObject) {
// add string object to campaignParam object
String textMessage = this.compileEmailTemplate(textMessageTemplate, "EmailCampaign", entry);
if (!textMessage.isEmpty()) {
final Integer totalMessage = runReportObject.size();
campaignMessage = new PreviewCampaignMessage().setCampaignMessage(textMessage)
.setTotalNumberOfMessages(totalMessage);
break;
}
}
}
} catch (final IOException e) {
// TODO throw something here
}
return campaignMessage;
}
@Transactional
@Override
public CommandProcessingResult reactivateEmailCampaign(final Long campaignId, JsonCommand command) {
this.emailCampaignValidator.validateActivation(command.json());
final AppUser currentUser = this.context.authenticatedUser();
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(campaignId)
.orElseThrow(() -> new EmailCampaignNotFound(campaignId));
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final LocalDate reactivationDate = command.localDateValueOfParameterNamed("activationDate");
emailCampaign.reactivate(currentUser, fmt, reactivationDate);
if (emailCampaign.isSchedule()) {
// if recurrence start date is in the past, calculate next trigger date, otherwise use recurrence start date
// as next trigger date when activating
LocalDateTime nextTriggerDate = null;
LocalDateTime tenantDateTime = DateUtils.getLocalDateTimeOfTenant();
LocalDateTime recurrenceStartDate = emailCampaign.getRecurrenceStartDate();
if (DateUtils.isBefore(recurrenceStartDate, tenantDateTime)) {
nextTriggerDate = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), recurrenceStartDate, tenantDateTime);
} else {
nextTriggerDate = recurrenceStartDate;
}
final String dateString = nextTriggerDate.toString() + " " + recurrenceStartDate.getHour() + ":"
+ recurrenceStartDate.getMinute() + ":" + recurrenceStartDate.getSecond();
final DateTimeFormatter simpleDateFormat = new DateTimeFormatterBuilder().parseCaseInsensitive().parseLenient()
.appendPattern("yyyy-MM-dd HH:mm:ss").toFormatter();
final LocalDateTime nextTriggerDateWithTime = LocalDateTime.parse(dateString, simpleDateFormat);
emailCampaign.setNextTriggerDate(nextTriggerDateWithTime);
this.emailCampaignRepository.saveAndFlush(emailCampaign);
}
return new CommandProcessingResultBuilder() //
.withEntityId(emailCampaign.getId()) //
.build();
}
private void handleDataIntegrityIssues(@SuppressWarnings("unused") final JsonCommand command, final Throwable realCause,
final NonTransientDataAccessException dve) {
throw ErrorHandler.getMappable(dve, "error.msg.email.campaign.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
}