| /** |
| * 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.portfolio.calendar.service; |
| |
| import java.sql.ResultSet; |
| import java.sql.SQLException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| |
| import org.apache.fineract.infrastructure.core.data.EnumOptionData; |
| import org.apache.fineract.infrastructure.core.domain.JdbcSupport; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.infrastructure.core.service.RoutingDataSource; |
| import org.apache.fineract.portfolio.calendar.data.CalendarData; |
| import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; |
| import org.apache.fineract.portfolio.calendar.domain.CalendarType; |
| import org.apache.fineract.portfolio.calendar.exception.CalendarNotFoundException; |
| import org.apache.fineract.portfolio.meeting.data.MeetingData; |
| import org.joda.time.LocalDate; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.dao.EmptyResultDataAccessException; |
| import org.springframework.jdbc.core.JdbcTemplate; |
| import org.springframework.jdbc.core.RowMapper; |
| import org.springframework.stereotype.Service; |
| import org.springframework.util.CollectionUtils; |
| |
| @Service |
| public class CalendarReadPlatformServiceImpl implements CalendarReadPlatformService { |
| |
| private final JdbcTemplate jdbcTemplate; |
| |
| @Autowired |
| public CalendarReadPlatformServiceImpl(final RoutingDataSource dataSource) { |
| this.jdbcTemplate = new JdbcTemplate(dataSource); |
| } |
| |
| private static final class CalendarDataMapper implements RowMapper<CalendarData> { |
| |
| public String schema() { |
| return " select c.id as id, ci.id as calendarInstanceId, ci.entity_id as entityId, ci.entity_type_enum as entityTypeId, c.title as title, " |
| + " c.description as description, c.location as location, c.start_date as startDate, c.end_date as endDate, " |
| + " c.duration as duration, c.calendar_type_enum as typeId, c.repeating as repeating, " |
| + " c.recurrence as recurrence, c.remind_by_enum as remindById, c.first_reminder as firstReminder, c.second_reminder as secondReminder, " |
| + " c.created_date as createdDate, c.lastmodified_date as updatedDate, creatingUser.id as creatingUserId, creatingUser.username as creatingUserName, " |
| + " updatingUser.id as updatingUserId, updatingUser.username as updatingUserName " |
| + " from m_calendar c join m_calendar_instance ci on ci.calendar_id=c.id, m_appuser as creatingUser, m_appuser as updatingUser" |
| + " where c.createdby_id=creatingUser.id and c.lastmodifiedby_id=updatingUser.id "; |
| } |
| |
| @Override |
| public CalendarData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { |
| |
| final Long id = rs.getLong("id"); |
| final Long calendarInstanceId = rs.getLong("calendarInstanceId"); |
| final Long entityId = rs.getLong("entityId"); |
| final Integer entityTypeId = rs.getInt("entityTypeId"); |
| final EnumOptionData entityType = CalendarEnumerations.calendarEntityType(entityTypeId); |
| final String title = rs.getString("title"); |
| final String description = rs.getString("description"); |
| final String location = rs.getString("location"); |
| final LocalDate startDate = JdbcSupport.getLocalDate(rs, "startDate"); |
| final LocalDate endDate = JdbcSupport.getLocalDate(rs, "endDate"); |
| final Integer duration = rs.getInt("duration"); |
| final Integer typeId = rs.getInt("typeId"); |
| final EnumOptionData type = CalendarEnumerations.calendarType(typeId); |
| final boolean repeating = rs.getBoolean("repeating"); |
| final String recurrence = rs.getString("recurrence"); |
| final EnumOptionData frequency = CalendarEnumerations.calendarFrequencyType(CalendarUtils.getFrequency(recurrence)); |
| final Integer interval = new Integer(CalendarUtils.getInterval(recurrence)); |
| final EnumOptionData repeatsOnDay = CalendarEnumerations.calendarWeekDaysType(CalendarUtils.getRepeatsOnDay(recurrence)); |
| final Integer remindById = rs.getInt("remindById"); |
| EnumOptionData remindBy = null; |
| if (remindById != null && remindById != 0) { |
| remindBy = CalendarEnumerations.calendarRemindBy(remindById); |
| } |
| final Integer firstReminder = rs.getInt("firstReminder"); |
| final Integer secondReminder = rs.getInt("secondReminder"); |
| String humanReadable = null; |
| if (startDate != null && recurrence != null) { |
| humanReadable = CalendarUtils.getRRuleReadable(startDate, recurrence); |
| } |
| |
| final LocalDate createdDate = JdbcSupport.getLocalDate(rs, "createdDate"); |
| final LocalDate lastUpdatedDate = JdbcSupport.getLocalDate(rs, "updatedDate"); |
| final Long createdByUserId = rs.getLong("creatingUserId"); |
| final String createdByUserName = rs.getString("creatingUserName"); |
| final Long lastUpdatedByUserId = rs.getLong("updatingUserId"); |
| final String lastUpdatedByUserName = rs.getString("updatingUserName"); |
| |
| return CalendarData.instance(id, calendarInstanceId, entityId, entityType, title, description, location, startDate, endDate, |
| duration, type, repeating, recurrence, frequency, interval, repeatsOnDay, remindBy, firstReminder, secondReminder, |
| humanReadable, createdDate, lastUpdatedDate, createdByUserId, createdByUserName, lastUpdatedByUserId, |
| lastUpdatedByUserName); |
| } |
| } |
| |
| @Override |
| public CalendarData retrieveCalendar(final Long calendarId, final Long entityId, final Integer entityTypeId) { |
| |
| try { |
| final CalendarDataMapper rm = new CalendarDataMapper(); |
| |
| final String sql = rm.schema() + " and c.id = ? and ci.entity_id = ? and ci.entity_type_enum = ? "; |
| |
| return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { calendarId, entityId, entityTypeId }); |
| } catch (final EmptyResultDataAccessException e) { |
| throw new CalendarNotFoundException(calendarId); |
| } |
| } |
| |
| @Override |
| public Collection<CalendarData> retrieveCalendarsByEntity(final Long entityId, final Integer entityTypeId, |
| final List<Integer> calendarTypeOptions) { |
| final CalendarDataMapper rm = new CalendarDataMapper(); |
| |
| Collection<CalendarData> result = null; |
| |
| String sql = ""; |
| |
| if (calendarTypeOptions == null || calendarTypeOptions.isEmpty()) { |
| sql = rm.schema() + " and ci.entity_id = ? and ci.entity_type_enum = ? order by c.start_date "; |
| result = this.jdbcTemplate.query(sql, rm, new Object[] { entityId, entityTypeId }); |
| } else if (!calendarTypeOptions.isEmpty()) { |
| final String sqlCalendarTypeOptions = CalendarUtils.getSqlCalendarTypeOptionsInString(calendarTypeOptions); |
| sql = rm.schema() + " and ci.entity_id = ? and ci.entity_type_enum = ? and c.calendar_type_enum in ( " + sqlCalendarTypeOptions |
| + " ) order by c.start_date "; |
| result = this.jdbcTemplate.query(sql, rm, new Object[] { entityId, entityTypeId }); |
| } |
| return result; |
| } |
| |
| @Override |
| public CalendarData retrieveCollctionCalendarByEntity(final Long entityId, final Integer entityTypeId) { |
| final CalendarDataMapper rm = new CalendarDataMapper(); |
| |
| final String sql = rm.schema() |
| + " and ci.entity_id = ? and ci.entity_type_enum = ? and calendar_type_enum = ? order by c.start_date "; |
| final List<CalendarData> result = this.jdbcTemplate.query(sql, rm, |
| new Object[] { entityId, entityTypeId, CalendarType.COLLECTION.getValue() }); |
| |
| if (!result.isEmpty() && result.size() > 0) { return result.get(0); } |
| |
| return null; |
| } |
| |
| @Override |
| public Collection<CalendarData> retrieveParentCalendarsByEntity(final Long entityId, final Integer entityTypeId, |
| final List<Integer> calendarTypeOptions) { |
| |
| final CalendarDataMapper rm = new CalendarDataMapper(); |
| Collection<CalendarData> result = null; |
| String sql = ""; |
| final CalendarEntityType ceType = CalendarEntityType.fromInt(entityTypeId); |
| final String parentHeirarchyCondition = getParentHierarchyCondition(ceType); |
| |
| // FIXME :AA center is the parent entity of group, change this code to |
| // support more parent entity types. |
| if (calendarTypeOptions == null || calendarTypeOptions.isEmpty()) { |
| sql = rm.schema() + " " + parentHeirarchyCondition + " and ci.entity_type_enum = ? order by c.start_date "; |
| result = this.jdbcTemplate.query(sql, rm, new Object[] { entityId, CalendarEntityType.CENTERS.getValue() }); |
| } else { |
| final String sqlCalendarTypeOptions = CalendarUtils.getSqlCalendarTypeOptionsInString(calendarTypeOptions); |
| sql = rm.schema() + " " + parentHeirarchyCondition + " and ci.entity_type_enum = ? and c.calendar_type_enum in (" |
| + sqlCalendarTypeOptions + ") order by c.start_date "; |
| result = this.jdbcTemplate.query(sql, rm, new Object[] { entityId, CalendarEntityType.CENTERS.getValue() }); |
| } |
| return result; |
| } |
| |
| @Override |
| public Collection<CalendarData> retrieveAllCalendars() { |
| |
| final CalendarDataMapper rm = new CalendarDataMapper(); |
| |
| final String sql = rm.schema(); |
| |
| return this.jdbcTemplate.query(sql, rm); |
| } |
| |
| @Override |
| public CalendarData retrieveNewCalendarDetails() { |
| return CalendarData.sensibleDefaultsForNewCalendarCreation(); |
| } |
| |
| @Override |
| public Collection<LocalDate> generateRecurringDates(final CalendarData calendarData, final boolean withHistory, final LocalDate tillDate) { |
| final LocalDate fromDate = null; |
| Collection<LocalDate> recurringDates = generateRecurringDate(calendarData, fromDate, tillDate, -1); |
| |
| if (withHistory) { |
| final Collection<CalendarData> calendarHistorys = this.retrieveCalendarsFromHistory(calendarData.getId()); |
| for (CalendarData calendarHistory : calendarHistorys) { |
| recurringDates.addAll(generateRecurringDate(calendarHistory, fromDate, tillDate, -1)); |
| } |
| } |
| |
| return recurringDates; |
| } |
| |
| @Override |
| public Collection<LocalDate> generateNextTenRecurringDates(CalendarData calendarData) { |
| final LocalDate tillDate = null; |
| return generateRecurringDate(calendarData, DateUtils.getLocalDateOfTenant(), tillDate, 10); |
| } |
| |
| private Collection<LocalDate> generateRecurringDate(final CalendarData calendarData, final LocalDate fromDate, |
| final LocalDate tillDate, final int maxCount) { |
| |
| if (!calendarData.isRepeating()) { return null; } |
| final String rrule = calendarData.getRecurrence(); |
| /** |
| * Start date or effective from date of calendar recurrence. |
| */ |
| final LocalDate seedDate = this.getSeedDate(calendarData.getStartDate()); |
| /** |
| * periodStartDate date onwards recurring dates will be generated. |
| */ |
| final LocalDate periodStartDate = this.getPeriodStartDate(seedDate, calendarData.getStartDate(), fromDate); |
| /** |
| * till periodEndDate recurring dates will be generated. |
| */ |
| final LocalDate periodEndDate = this.getPeriodEndDate(calendarData.getEndDate(), tillDate); |
| |
| final Collection<LocalDate> recurringDates = CalendarUtils.getRecurringDates(rrule, seedDate, periodStartDate, periodEndDate, |
| maxCount); |
| return recurringDates; |
| } |
| |
| private LocalDate getSeedDate(LocalDate date) { |
| return date; |
| } |
| |
| private LocalDate getPeriodStartDate(final LocalDate seedDate, final LocalDate recurrenceStartDate, final LocalDate fromDate) { |
| LocalDate periodStartDate = null; |
| if (fromDate != null) { |
| periodStartDate = fromDate; |
| } else { |
| final LocalDate currentDate = DateUtils.getLocalDateOfTenant(); |
| if (seedDate.isBefore(currentDate.minusYears(1))) { |
| periodStartDate = currentDate.minusYears(1); |
| } else { |
| periodStartDate = recurrenceStartDate; |
| } |
| } |
| return periodStartDate; |
| } |
| |
| private LocalDate getPeriodEndDate(LocalDate endDate, LocalDate tillDate) { |
| LocalDate periodEndDate = endDate; |
| final LocalDate currentDate = DateUtils.getLocalDateOfTenant(); |
| |
| if (tillDate != null) { |
| if (endDate != null) { |
| if (endDate.isAfter(tillDate)) { |
| // to retrieve meeting dates tillspecified date (tillDate) |
| periodEndDate = tillDate; |
| } |
| } else { |
| // end date is null then fetch meeting dates tillDate |
| periodEndDate = tillDate; |
| } |
| } else if (endDate == null || endDate.isAfter(currentDate.plusYears(1))) { |
| periodEndDate = currentDate.plusYears(1); |
| } |
| return periodEndDate; |
| } |
| |
| @Override |
| public LocalDate generateNextEligibleMeetingDateForCollection(final CalendarData calendarData, final MeetingData lastMeetingData) { |
| |
| final LocalDate lastMeetingDate = (lastMeetingData == null) ? null : lastMeetingData.getMeetingDate(); |
| // get applicable calendar based on meeting date |
| CalendarData applicableCalendarData = calendarData; |
| LocalDate nextEligibleMeetingDate = null; |
| /** |
| * The next available meeting date for collection should be taken from |
| * application calendar for that time period. e.g. If the previous |
| * calendar details has weekly meeting starting from 1st of Oct 2013 on |
| * every Tuesday, then meeting dates for collection are 1,8,15,22,29.. |
| * |
| * If meeting schedule has changed from Tuesday to Friday with effective |
| * from 15th of Oct (calendar update has made on 2nd of Oct) , then |
| * application should allow to generate collection sheet on 8th of Oct |
| * which is still on Tuesday and next collection sheet date should be on |
| * 18th of Oct as per current calendar |
| */ |
| if (lastMeetingDate != null && !calendarData.isBetweenStartAndEndDate(lastMeetingDate) |
| && !calendarData.isBetweenStartAndEndDate(DateUtils.getLocalDateOfTenant())) { |
| applicableCalendarData = this.retrieveApplicableCalendarFromHistory(calendarData.getId(), lastMeetingDate); |
| nextEligibleMeetingDate = CalendarUtils.getRecentEligibleMeetingDate(applicableCalendarData.getRecurrence(), lastMeetingDate); |
| } |
| |
| /** |
| * If nextEligibleMeetingDate is on or after current calendar startdate |
| * then regenerate the nextEligible meeting date based on |
| */ |
| if (nextEligibleMeetingDate == null) { |
| final LocalDate seedDate = (lastMeetingDate != null) ? lastMeetingDate : calendarData.getStartDate(); |
| nextEligibleMeetingDate = CalendarUtils.getRecentEligibleMeetingDate(applicableCalendarData.getRecurrence(), seedDate); |
| } else if (calendarData.isBetweenStartAndEndDate(nextEligibleMeetingDate)) { |
| nextEligibleMeetingDate = CalendarUtils.getRecentEligibleMeetingDate(applicableCalendarData.getRecurrence(), |
| calendarData.getStartDate()); |
| } |
| |
| return nextEligibleMeetingDate; |
| } |
| |
| @Override |
| public Collection<CalendarData> updateWithRecurringDates(final Collection<CalendarData> calendarsData) { |
| final Collection<CalendarData> recuCalendarsData = new ArrayList<>(); |
| final boolean withHistory = true; |
| final LocalDate tillDate = null; |
| for (final CalendarData calendarData : calendarsData) { |
| final Collection<LocalDate> recurringDates = this.generateRecurringDates(calendarData, withHistory, tillDate); |
| final Collection<LocalDate> nextTenRecurringDates = this.generateNextTenRecurringDates(calendarData); |
| final LocalDate recentEligibleMeetingDate = null; |
| final CalendarData updatedCalendarData = CalendarData.withRecurringDates(calendarData, recurringDates, nextTenRecurringDates, |
| recentEligibleMeetingDate); |
| recuCalendarsData.add(updatedCalendarData); |
| } |
| |
| return recuCalendarsData; |
| } |
| |
| @Override |
| public CalendarData retrieveLoanCalendar(final Long loanId) { |
| final CalendarDataMapper rm = new CalendarDataMapper(); |
| |
| final String sql = rm.schema() + " and ci.entity_id = ? and ci.entity_type_enum = ? order by c.start_date "; |
| CalendarData calendarData = null; |
| final Collection<CalendarData> calendars = this.jdbcTemplate.query(sql, rm, |
| new Object[] { loanId, CalendarEntityType.LOANS.getValue() }); |
| |
| if (!CollectionUtils.isEmpty(calendars)) { |
| for (final CalendarData calendar : calendars) { |
| calendarData = calendar; |
| break;// Loans are associated with only one calendar |
| } |
| } |
| |
| return calendarData; |
| } |
| |
| public static String getParentHierarchyCondition(final CalendarEntityType calendarEntityType) { |
| String conditionSql = ""; |
| |
| switch (calendarEntityType) { |
| case CLIENTS: |
| // TODO : AA : do we need to propagate to top level parent in |
| // hierarchy? |
| conditionSql = " and ci.entity_id in (select gc.group_id from m_client c join m_group_client gc " |
| + " on c.id=gc.client_id where c.id = ? ) "; |
| break; |
| |
| case GROUPS: |
| // TODO : AA: add parent hierarchy for groups |
| conditionSql = " and ci.entity_id in (select g.parent_id from m_group g where g.id = ? ) "; |
| break; |
| |
| case LOANS: |
| // TODO : AA: do we need parent hierarchy calendars for loans? |
| conditionSql = " and ci.entity_id = ? "; |
| break; |
| |
| default: |
| break; |
| } |
| |
| return conditionSql; |
| } |
| |
| private CalendarData retrieveApplicableCalendarFromHistory(Long calendarId, LocalDate compareDate) { |
| try { |
| final CalendarDataFromHistoryMapper rm = new CalendarDataFromHistoryMapper(); |
| |
| final String sql = rm.schema() + " where c.calendar_id = ? and date(?) between c.start_date and c.end_date limit 1"; |
| |
| return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { calendarId, compareDate.toDate() }); |
| } catch (final EmptyResultDataAccessException e) { |
| return null; |
| } |
| } |
| |
| private Collection<CalendarData> retrieveCalendarsFromHistory(Long calendarId) { |
| try { |
| final CalendarDataFromHistoryMapper rm = new CalendarDataFromHistoryMapper(); |
| |
| final String sql = rm.schema() + " where c.calendar_id = ? "; |
| |
| final Collection<CalendarData> calendars = this.jdbcTemplate.query(sql, rm, new Object[] { calendarId }); |
| return calendars; |
| } catch (final EmptyResultDataAccessException e) { |
| return null; |
| } |
| } |
| |
| private static final class CalendarDataFromHistoryMapper implements RowMapper<CalendarData> { |
| |
| public String schema() { |
| return " select c.calendar_id as id, c.title as title, c.description as description, c.location as location, c.start_date as startDate, " |
| + " c.end_date as endDate, c.duration as duration, c.calendar_type_enum as typeId, c.repeating as repeating, " |
| + " c.recurrence as recurrence, c.remind_by_enum as remindById, c.first_reminder as firstReminder, c.second_reminder as secondReminder " |
| + " from m_calendar_history c "; |
| } |
| |
| @Override |
| public CalendarData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { |
| |
| final Long id = rs.getLong("id"); |
| final Long calendarInstanceId = null; |
| final Long entityId = null; |
| final EnumOptionData entityType = null; |
| final String title = rs.getString("title"); |
| final String description = rs.getString("description"); |
| final String location = rs.getString("location"); |
| final LocalDate startDate = JdbcSupport.getLocalDate(rs, "startDate"); |
| final LocalDate endDate = JdbcSupport.getLocalDate(rs, "endDate"); |
| final Integer duration = rs.getInt("duration"); |
| final Integer typeId = rs.getInt("typeId"); |
| final EnumOptionData type = CalendarEnumerations.calendarType(typeId); |
| final boolean repeating = rs.getBoolean("repeating"); |
| final String recurrence = rs.getString("recurrence"); |
| final EnumOptionData frequency = CalendarEnumerations.calendarFrequencyType(CalendarUtils.getFrequency(recurrence)); |
| final Integer interval = new Integer(CalendarUtils.getInterval(recurrence)); |
| final EnumOptionData repeatsOnDay = CalendarEnumerations.calendarWeekDaysType(CalendarUtils.getRepeatsOnDay(recurrence)); |
| final Integer remindById = rs.getInt("remindById"); |
| EnumOptionData remindBy = null; |
| if (remindById != null && remindById != 0) { |
| remindBy = CalendarEnumerations.calendarRemindBy(remindById); |
| } |
| final Integer firstReminder = rs.getInt("firstReminder"); |
| final Integer secondReminder = rs.getInt("secondReminder"); |
| String humanReadable = null; |
| if (startDate != null && recurrence != null) { |
| humanReadable = CalendarUtils.getRRuleReadable(startDate, recurrence); |
| } |
| |
| final LocalDate createdDate = null; |
| final LocalDate lastUpdatedDate = null; |
| final Long createdByUserId = null; |
| final String createdByUserName = null; |
| final Long lastUpdatedByUserId = null; |
| final String lastUpdatedByUserName = null; |
| |
| return CalendarData.instance(id, calendarInstanceId, entityId, entityType, title, description, location, startDate, endDate, |
| duration, type, repeating, recurrence, frequency, interval, repeatsOnDay, remindBy, firstReminder, secondReminder, |
| humanReadable, createdDate, lastUpdatedDate, createdByUserId, createdByUserName, lastUpdatedByUserId, |
| lastUpdatedByUserName); |
| } |
| } |
| } |