org.mifosplatform.infrastructure.reportmailingjob.service.ReportMailingJobWritePlatformServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.mifosplatform.infrastructure.reportmailingjob.service.ReportMailingJobWritePlatformServiceImpl.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package org.mifosplatform.infrastructure.reportmailingjob.service;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeFieldType;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.dataqueries.domain.Report;
import org.mifosplatform.infrastructure.dataqueries.domain.ReportRepositoryWrapper;
import org.mifosplatform.infrastructure.dataqueries.service.ReadReportingService;
import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.infrastructure.reportmailingjob.ReportMailingJobConstants;
import org.mifosplatform.infrastructure.reportmailingjob.data.ReportMailingJobEmailData;
import org.mifosplatform.infrastructure.reportmailingjob.data.ReportMailingJobValidator;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJob;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobEmailAttachmentFileFormat;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobPreviousRunStatus;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobRepository;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobRepositoryWrapper;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistory;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistoryRepository;
import org.mifosplatform.infrastructure.reportmailingjob.domain.ReportMailingJobStretchyReportParamDateOption;
import org.mifosplatform.infrastructure.reportmailingjob.helper.IPv4Helper;
import org.mifosplatform.infrastructure.reportmailingjob.helper.ReportMailingJobStretchyReportDateHelper;
import org.mifosplatform.infrastructure.security.service.BasicAuthTenantDetailsService;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJobWritePlatformService {

    private final static Logger logger = LoggerFactory.getLogger(ReportMailingJobWritePlatformServiceImpl.class);
    private final ReportRepositoryWrapper reportRepositoryWrapper;
    private final ReportMailingJobValidator reportMailingJobValidator;
    private final ReportMailingJobRepositoryWrapper reportMailingJobRepositoryWrapper;
    private final ReportMailingJobRepository reportMailingJobRepository;
    private final PlatformSecurityContext platformSecurityContext;
    private final ReportMailingJobEmailService reportMailingJobEmailService;
    private final ReadReportingService readReportingService;
    private final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository;
    private final BasicAuthTenantDetailsService basicAuthTenantDetailsService;

    @Autowired
    public ReportMailingJobWritePlatformServiceImpl(final ReportRepositoryWrapper reportRepositoryWrapper,
            final ReportMailingJobValidator reportMailingJobValidator,
            final ReportMailingJobRepositoryWrapper reportMailingJobRepositoryWrapper,
            final ReportMailingJobRepository reportMailingJobRepository,
            final PlatformSecurityContext platformSecurityContext,
            final ReportMailingJobEmailService reportMailingJobEmailService,
            final ReadReportingService readReportingService,
            final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository,
            final BasicAuthTenantDetailsService basicAuthTenantDetailsService) {
        this.reportRepositoryWrapper = reportRepositoryWrapper;
        this.reportMailingJobValidator = reportMailingJobValidator;
        this.reportMailingJobRepositoryWrapper = reportMailingJobRepositoryWrapper;
        this.reportMailingJobRepository = reportMailingJobRepositoryWrapper.getReportMailingJobRepository();
        this.platformSecurityContext = platformSecurityContext;
        this.reportMailingJobEmailService = reportMailingJobEmailService;
        this.readReportingService = readReportingService;
        this.reportMailingJobRunHistoryRepository = reportMailingJobRunHistoryRepository;
        this.basicAuthTenantDetailsService = basicAuthTenantDetailsService;
    }

    @Override
    @Transactional
    public CommandProcessingResult createReportMailingJob(JsonCommand jsonCommand) {
        try {
            // validate the create request
            this.reportMailingJobValidator.validateCreateRequest(jsonCommand);

            final AppUser appUser = this.platformSecurityContext.authenticatedUser();

            // get the stretchy Report object
            final Report stretchyReport = this.reportRepositoryWrapper.findOneThrowExceptionIfNotFound(
                    jsonCommand.longValueOfParameterNamed(ReportMailingJobConstants.STRETCHY_REPORT_ID_PARAM_NAME));

            // create an instance of ReportMailingJob class from the JsonCommand object
            final ReportMailingJob reportMailingJob = ReportMailingJob.instance(jsonCommand, stretchyReport,
                    new LocalDate(), appUser, appUser);

            // save entity
            this.reportMailingJobRepository.save(reportMailingJob);

            return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId())
                    .withEntityId(reportMailingJob.getId()).build();
        }

        catch (final DataIntegrityViolationException dve) {
            handleDataIntegrityIssues(jsonCommand, dve);

            return CommandProcessingResult.empty();
        }
    }

    @Override
    @Transactional
    public CommandProcessingResult updateReportMailingJob(Long reportMailingJobId, JsonCommand jsonCommand) {
        try {
            // validate the update request
            this.reportMailingJobValidator.validateUpdateRequest(jsonCommand);

            // retrieve the ReportMailingJob object from the database
            final ReportMailingJob reportMailingJob = this.reportMailingJobRepositoryWrapper
                    .findOneThrowExceptionIfNotFound(reportMailingJobId);

            final Map<String, Object> changes = reportMailingJob.update(jsonCommand);

            // get the recurrence rule string
            final String recurrence = reportMailingJob.getRecurrence();

            // get the next run DateTime from the ReportMailingJob entity
            DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();

            // get the start DateTime from the ReportMailingJob entity
            DateTime startDateTime = reportMailingJob.getStartDateTime();

            if (!changes.isEmpty()) {
                // check if the stretchy report id was updated
                if (changes.containsKey(ReportMailingJobConstants.STRETCHY_REPORT_ID_PARAM_NAME)) {
                    final Long stretchyReportId = (Long) changes
                            .get(ReportMailingJobConstants.STRETCHY_REPORT_ID_PARAM_NAME);
                    final Report stretchyReport = this.reportRepositoryWrapper
                            .findOneThrowExceptionIfNotFound(stretchyReportId);

                    // update the stretchy report
                    reportMailingJob.update(stretchyReport);
                }

                // check if the recurrence was updated
                if (changes.containsKey(ReportMailingJobConstants.RECURRENCE_PARAM_NAME)) {

                    // go ahead if the recurrence is not null
                    if (StringUtils.isNotBlank(recurrence)) {
                        // check if the start DateTime was updated
                        if (changes.containsKey(ReportMailingJobConstants.START_DATE_TIME_PARAM_NAME)) {
                            // get the updated start DateTime
                            startDateTime = reportMailingJob.getStartDateTime();
                        }

                        // get the next recurring DateTime
                        final DateTime nextRecurringDateTime = this.createNextRecurringDateTime(reportMailingJob);

                        // update the next run time property
                        reportMailingJob.updateNextRunDateTime(nextRecurringDateTime);
                    }

                    // check if the next run DateTime is not empty and the recurrence is empty
                    else if (StringUtils.isBlank(recurrence) && (nextRunDateTime != null)) {
                        // the next run DateTime should be set to null
                        reportMailingJob.updateNextRunDateTime(null);
                    }
                }

                if (changes.containsKey(ReportMailingJobConstants.START_DATE_TIME_PARAM_NAME)) {
                    startDateTime = reportMailingJob.getStartDateTime();

                    // initially set the next recurring date time to the new start date time
                    DateTime nextRecurringDateTime = startDateTime;

                    // ensure that the recurrence pattern string is not empty
                    if (StringUtils.isNotBlank(recurrence)) {
                        // get the next recurring DateTime
                        nextRecurringDateTime = this.createNextRecurringDateTime(reportMailingJob);
                    }

                    // update the next run time property
                    reportMailingJob.updateNextRunDateTime(nextRecurringDateTime);
                }

                // save and flush immediately so any data integrity exception can be handled in the "catch" block
                this.reportMailingJobRepository.saveAndFlush(reportMailingJob);
            }

            return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId())
                    .withEntityId(reportMailingJob.getId()).with(changes).build();
        }

        catch (final DataIntegrityViolationException dve) {
            handleDataIntegrityIssues(jsonCommand, dve);

            return CommandProcessingResult.empty();
        }
    }

    @Override
    @Transactional
    public CommandProcessingResult deleteReportMailingJob(Long reportMailingJobId) {
        // retrieve the ReportMailingJob object from the database
        final ReportMailingJob reportMailingJob = this.reportMailingJobRepositoryWrapper
                .findOneThrowExceptionIfNotFound(reportMailingJobId);

        // delete the report mailing job by setting the isDeleted property to 1 and altering the name
        reportMailingJob.delete();

        // save the report mailing job entity
        this.reportMailingJobRepository.save(reportMailingJob);

        return new CommandProcessingResultBuilder().withEntityId(reportMailingJobId).build();
    }

    @Override
    @CronTarget(jobName = JobName.EXECUTE_REPORT_MAILING_JOBS)
    public void executeReportMailingJobs() throws JobExecutionException {
        if (IPv4Helper.applicationIsNotRunningOnLocalMachine()) {
            final Collection<ReportMailingJob> reportMailingJobCollection = this.reportMailingJobRepository
                    .findAll();

            for (ReportMailingJob reportMailingJob : reportMailingJobCollection) {
                // get the tenant's date as a DateTime object
                final DateTime localDateTimeOftenant = DateUtils.getLocalDateTimeOfTenant().toDateTime();

                final DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();

                if (nextRunDateTime != null && reportMailingJob.isActive() && reportMailingJob.isNotDeleted()) {

                    if (nextRunDateTime.isBefore(localDateTimeOftenant)
                            || nextRunDateTime.isEqual(localDateTimeOftenant)) {
                        // get the emailAttachmentFileFormat enum object
                        final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat = ReportMailingJobEmailAttachmentFileFormat
                                .instance(reportMailingJob.getEmailAttachmentFileFormat());

                        if (emailAttachmentFileFormat != null
                                && Arrays.asList(ReportMailingJobEmailAttachmentFileFormat.validValues())
                                        .contains(emailAttachmentFileFormat.getId())) {
                            final Report stretchyReport = reportMailingJob.getStretchyReport();
                            final String reportName = (stretchyReport != null) ? stretchyReport.getReportName()
                                    : null;
                            final StringBuilder errorLog = new StringBuilder();
                            final Map<String, String> validateStretchyReportParamMap = this.reportMailingJobValidator
                                    .validateStretchyReportParamMap(reportMailingJob.getStretchyReportParamMap());
                            Map<String, String> reportParams = new HashMap<>();

                            if (validateStretchyReportParamMap != null) {
                                Iterator<Map.Entry<String, String>> validateStretchyReportParamMapEntries = validateStretchyReportParamMap
                                        .entrySet().iterator();

                                while (validateStretchyReportParamMapEntries.hasNext()) {
                                    Map.Entry<String, String> validateStretchyReportParamMapEntry = validateStretchyReportParamMapEntries
                                            .next();
                                    String key = validateStretchyReportParamMapEntry.getKey();
                                    String value = validateStretchyReportParamMapEntry.getValue();
                                    Object[] stretchyReportParamDateOptionsList = ReportMailingJobStretchyReportParamDateOption
                                            .validValues();

                                    if (StringUtils.containsIgnoreCase(key, "date")) {
                                        if (Arrays.asList(stretchyReportParamDateOptionsList).contains(value)) {
                                            ReportMailingJobStretchyReportParamDateOption enumOption = ReportMailingJobStretchyReportParamDateOption
                                                    .instance(value);

                                            value = ReportMailingJobStretchyReportDateHelper
                                                    .getDateAsString(enumOption);
                                        }
                                    }

                                    reportParams.put(key, value);
                                }
                            }

                            // generate the pentaho report output stream, method in turn call another that sends the file to the email recipients
                            this.generatePentahoReportOutputStream(reportMailingJob, emailAttachmentFileFormat,
                                    reportParams, reportName, errorLog);

                            // TODO - write a helper method to handle the generation of the pentaho report file
                            this.updateReportMailingJobAfterJobExecution(reportMailingJob, errorLog,
                                    localDateTimeOftenant);
                        }
                    }
                }
            }
        }
    }

    /** 
     * update the report mailing job entity after job execution 
     * 
     * @param reportMailingJob -- the report mailing job entity
     * @param errorLog -- StringBuilder object containing the error log if any
     * @param jobStartDateTime -- the start DateTime of the job
     * @return None
     **/
    private void updateReportMailingJobAfterJobExecution(final ReportMailingJob reportMailingJob,
            final StringBuilder errorLog, final DateTime jobStartDateTime) {
        this.switchToWriteDatabase();

        final String recurrence = reportMailingJob.getRecurrence();
        final DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();
        ReportMailingJobPreviousRunStatus reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.SUCCESS;

        reportMailingJob.updatePreviousRunErrorLog(null);

        if (errorLog != null && errorLog.length() > 0) {
            reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.ERROR;
            reportMailingJob.updatePreviousRunErrorLog(errorLog.toString());
        }

        reportMailingJob.increaseNumberOfRunsByOne();
        reportMailingJob.updatePreviousRunStatus(reportMailingJobPreviousRunStatus.getValue());
        reportMailingJob.updatePreviousRunDateTime(reportMailingJob.getNextRunDateTime());

        // check if the job has a recurrence pattern, if not deactivate the job. The job will only run once
        if (StringUtils.isEmpty(recurrence)) {
            // deactivate job
            reportMailingJob.deactivate();

            // job will only run once, no next run time
            reportMailingJob.updateNextRunDateTime(null);
        }

        else if (nextRunDateTime != null) {
            final DateTime nextRecurringDateTime = this.createNextRecurringDateTime(reportMailingJob);

            // finally update the next run date time property
            reportMailingJob.updateNextRunDateTime(nextRecurringDateTime);
        }

        // save the ReportMailingJob entity
        this.reportMailingJobRepository.save(reportMailingJob);

        // create a new report mailing job run history entity
        this.createReportMailingJobRunHistroryAfterJobExecution(reportMailingJob, errorLog, jobStartDateTime,
                reportMailingJobPreviousRunStatus.getValue());
    }

    /**
     * create the next recurring DateTime from recurrence pattern, start DateTime and current DateTime
     * 
     * @param recurrencePattern
     * @param startDateTime
     * @return DateTime object
     */
    private DateTime createNextRecurringDateTime(final ReportMailingJob reportMailingJob) {
        DateTime nextRecurringDateTime = null;
        final String recurrencePattern = reportMailingJob.getRecurrence();
        final DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();

        // the recurrence pattern/rule cannot be empty
        if (StringUtils.isNotBlank(recurrencePattern) && nextRunDateTime != null) {
            final LocalDate currentDate = DateUtils.getLocalDateOfTenant();
            final LocalDate nextRunDate = nextRunDateTime.toLocalDate();

            LocalDate nextRecurringLocalDate = CalendarUtils.getNextRecurringDate(recurrencePattern, nextRunDate,
                    nextRunDate);

            if (nextRecurringLocalDate.isBefore(currentDate)) {
                nextRecurringLocalDate = CalendarUtils.getNextRecurringDate(recurrencePattern, nextRunDate,
                        currentDate);
            }

            // get the date fields
            final int currentYearIntValue = nextRecurringLocalDate.get(DateTimeFieldType.year());
            final int currentMonthIntValue = nextRecurringLocalDate.get(DateTimeFieldType.monthOfYear());
            final int currentDayIntValue = nextRecurringLocalDate.get(DateTimeFieldType.dayOfMonth());

            // append the time of the previous next run date time
            nextRecurringDateTime = nextRunDateTime.withDate(currentYearIntValue, currentMonthIntValue,
                    currentDayIntValue);
        }

        return nextRecurringDateTime;
    }

    /** 
     * create a new report mailing job run history entity after job execution
     * 
     * @param reportMailingJob -- the report mailing job entity
     * @param errorLog -- StringBuilder object containing the error log if any
     * @param jobStartDateTime -- the start DateTime of the job
     * @param jobRunStatus -- the status of the job (success/error)
     * @return None
     **/
    private void createReportMailingJobRunHistroryAfterJobExecution(final ReportMailingJob reportMailingJob,
            final StringBuilder errorLog, final DateTime jobStartDateTime, final String jobRunStatus) {
        final DateTime jobEndDateTime = DateUtils.getLocalDateTimeOfTenant().toDateTime();
        final String errorLogToString = (errorLog != null) ? errorLog.toString() : null;
        final ReportMailingJobRunHistory reportMailingJobRunHistory = ReportMailingJobRunHistory
                .instance(reportMailingJob, jobStartDateTime, jobEndDateTime, jobRunStatus, null, errorLogToString);

        this.reportMailingJobRunHistoryRepository.save(reportMailingJobRunHistory);
    }

    /** 
     * Handle any SQL data integrity issue 
     *
     * @param jsonCommand -- JsonCommand object
     * @param dve -- data integrity exception object
     * @return None
     **/
    private void handleDataIntegrityIssues(final JsonCommand jsonCommand,
            final DataIntegrityViolationException dve) {
        final Throwable realCause = dve.getMostSpecificCause();

        if (realCause.getMessage().contains(ReportMailingJobConstants.NAME_PARAM_NAME)) {
            final String name = jsonCommand.stringValueOfParameterNamed(ReportMailingJobConstants.NAME_PARAM_NAME);
            throw new PlatformDataIntegrityException("error.msg.report.mailing.job.duplicate.name",
                    "Report mailing job with name `" + name + "` already exists",
                    ReportMailingJobConstants.NAME_PARAM_NAME, name);
        }

        logger.error(dve.getMessage(), dve);

        throw new PlatformDataIntegrityException("error.msg.charge.unknown.data.integrity.issue",
                "Unknown data integrity issue with resource: " + realCause.getMessage());
    }

    /** 
     * generate the Pentaho report output stream
     * 
     * @return StringBuilder object -- the error log StringBuilder object
     **/
    private StringBuilder generatePentahoReportOutputStream(final ReportMailingJob reportMailingJob,
            final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat,
            final Map<String, String> reportParams, final String reportName, final StringBuilder errorLog) {

        try {
            this.switchToReadOnlyDatabase();

            final ByteArrayOutputStream byteArrayOutputStream = this.readReportingService
                    .generatePentahoReportAsOutputStream(reportName, emailAttachmentFileFormat.getValue(),
                            reportParams, null, reportMailingJob.getRunAsUser(), errorLog);
            final MifosPlatformTenant platformTenant = ThreadLocalContextUtil.getTenant();
            final String fileLocation = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator
                    + platformTenant.getTenantIdentifier() + File.separator + "reportmailingjob";
            final String fileNameWithoutExtension = fileLocation + File.separator + reportName;

            // check if file directory exists, if not create directory
            if (!new File(fileLocation).isDirectory()) {
                new File(fileLocation).mkdirs();
            }

            if (byteArrayOutputStream.size() == 0) {
                errorLog.append("Pentaho report processing failed, empty output stream created");
            }

            else if (errorLog.length() == 0 && (byteArrayOutputStream.size() > 0)) {
                final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue();

                // send the file to email recipients
                this.sendPentahoReportFileToEmailRecipients(reportMailingJob, fileName, byteArrayOutputStream,
                        errorLog);
            }
        }

        catch (Exception e) {
            // do nothing for now
            errorLog.append(e);
        }

        return errorLog;
    }

    /** 
     * send Pentaho report file to email recipients
     * 
     * @return None
     **/
    private void sendPentahoReportFileToEmailRecipients(final ReportMailingJob reportMailingJob,
            final String fileName, final ByteArrayOutputStream byteArrayOutputStream,
            final StringBuilder errorLog) {
        final Set<String> emailRecipients = this.reportMailingJobValidator
                .validateEmailRecipients(reportMailingJob.getEmailRecipients());

        try {
            final File file = new File(fileName);
            final FileOutputStream outputStream = new FileOutputStream(file);
            byteArrayOutputStream.writeTo(outputStream);

            for (String emailRecipient : emailRecipients) {
                final ReportMailingJobEmailData reportMailingJobEmailData = new ReportMailingJobEmailData(
                        emailRecipient, reportMailingJob.getEmailMessage(), reportMailingJob.getEmailSubject(),
                        file);

                this.reportMailingJobEmailService.sendEmailWithAttachment(reportMailingJobEmailData);
            }

        } catch (IOException e) {
            errorLog.append(
                    "The ReportMailingJobWritePlatformServiceImpl.executeReportMailingJobs threw an IOException "
                            + "exception: " + e.getMessage() + " ---------- ");
        }
    }

    /**
     * Switches the database to the slave read only database
     */
    private void switchToReadOnlyDatabase() {
        ThreadLocalContextUtil.switchToPrimaryDataSource();

        final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
        final MifosPlatformTenant platformTenant = this.basicAuthTenantDetailsService
                .loadTenantById(tenantIdentifier, true);

        ThreadLocalContextUtil.switchToTenantSpecificDataSource(platformTenant);
    }

    /**
     * Switches the database to the master write database
     */
    private void switchToWriteDatabase() {
        ThreadLocalContextUtil.switchToPrimaryDataSource();

        final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
        final MifosPlatformTenant platformTenant = this.basicAuthTenantDetailsService
                .loadTenantById(tenantIdentifier, false);

        ThreadLocalContextUtil.switchToTenantSpecificDataSource(platformTenant);
    }
}