diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 6b6378e50a..f482e1661a 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1059,7 +1059,8 @@ "accounts.collection.get", "accounts.item.post", "feefineactions.item.post", - "circulation-storage.loans-history.collection.get" + "circulation-storage.loans-history.collection.get", + "calendar.endpoint.dates.get" ], "schedule": { "cron": "1 0 * * *", diff --git a/src/main/java/org/folio/circulation/domain/Loan.java b/src/main/java/org/folio/circulation/domain/Loan.java index 788b65b511..c213df13f4 100644 --- a/src/main/java/org/folio/circulation/domain/Loan.java +++ b/src/main/java/org/folio/circulation/domain/Loan.java @@ -74,7 +74,7 @@ import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.policy.LoanPolicy; import org.folio.circulation.domain.policy.OverdueFinePolicy; -import org.folio.circulation.domain.policy.OverdueFinePolicyRemindersPolicy; +import org.folio.circulation.domain.policy.RemindersPolicy; import org.folio.circulation.domain.policy.lostitem.LostItemPolicy; import org.folio.circulation.domain.representations.LoanProperties; import org.folio.circulation.support.results.Result; @@ -660,17 +660,16 @@ public Integer getLastReminderFeeBilledNumber() { return (Integer) getValueByPath(representation, REMINDERS, LAST_FEE_BILLED, BILL_NUMBER); } - public OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry getNextReminder() { + public RemindersPolicy.ReminderConfig getNextReminder() { Integer latestReminderNumber = getLastReminderFeeBilledNumber(); if (latestReminderNumber == null) { latestReminderNumber = 0; } - if (getOverdueFinePolicy().getRemindersPolicy() == null - || getOverdueFinePolicy().getRemindersPolicy().getReminderSchedule() == null) { + RemindersPolicy remindersPolicy = getOverdueFinePolicy().getRemindersPolicy(); + if (remindersPolicy == null || !remindersPolicy.hasReminderSchedule()) { return null; } else { - OverdueFinePolicyRemindersPolicy.ReminderSequence schedule = getOverdueFinePolicy().getRemindersPolicy().getReminderSchedule(); - return schedule.getEntryAfter(latestReminderNumber); + return remindersPolicy.getNextReminderAfter(latestReminderNumber); } } diff --git a/src/main/java/org/folio/circulation/domain/notice/schedule/ReminderFeeScheduledNoticeService.java b/src/main/java/org/folio/circulation/domain/notice/schedule/ReminderFeeScheduledNoticeService.java index 3a7ec395cd..4ea72a9312 100644 --- a/src/main/java/org/folio/circulation/domain/notice/schedule/ReminderFeeScheduledNoticeService.java +++ b/src/main/java/org/folio/circulation/domain/notice/schedule/ReminderFeeScheduledNoticeService.java @@ -5,12 +5,15 @@ import org.folio.circulation.domain.Loan; import org.folio.circulation.domain.LoanAndRelatedRecords; import org.folio.circulation.domain.notice.NoticeTiming; -import org.folio.circulation.domain.policy.OverdueFinePolicyRemindersPolicy; +import org.folio.circulation.domain.policy.RemindersPolicy.ReminderConfig; +import org.folio.circulation.infrastructure.storage.CalendarRepository; import org.folio.circulation.infrastructure.storage.notices.ScheduledNoticesRepository; import org.folio.circulation.support.results.Result; +import org.folio.circulation.support.Clients; import java.lang.invoke.MethodHandles; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import static org.folio.circulation.support.results.Result.succeeded; @@ -19,42 +22,48 @@ public class ReminderFeeScheduledNoticeService { protected static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private final ScheduledNoticesRepository scheduledNoticesRepository; + private final CalendarRepository calendarRepository; - public ReminderFeeScheduledNoticeService (ScheduledNoticesRepository scheduledNoticesRepository) { - this.scheduledNoticesRepository = scheduledNoticesRepository; + + public ReminderFeeScheduledNoticeService (Clients clients) { + this.scheduledNoticesRepository = ScheduledNoticesRepository.using(clients); + this.calendarRepository = new CalendarRepository(clients); } public Result scheduleFirstReminder(LoanAndRelatedRecords records) { + log.debug("scheduleFirstReminder:: parameters loanAndRelatedRecords: {}", + records); Loan loan = records.getLoan(); if (loan.getOverdueFinePolicy().isReminderFeesPolicy()) { - - OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry firstReminder = - loan.getOverdueFinePolicy().getRemindersPolicy().getReminderSequenceEntry(1); - - ScheduledNoticeConfig config = - new ScheduledNoticeConfig( - NoticeTiming.AFTER, - null, // recurrence handled using reminder fee policy - firstReminder.getNoticeTemplateId(), - firstReminder.getNoticeFormat(), - true); - - ScheduledNotice scheduledNotice = new ScheduledNotice( - UUID.randomUUID().toString(), - loan.getId(), - null, - loan.getUserId(), - null, - null, - TriggeringEvent.DUE_DATE_WITH_REMINDER_FEE, - firstReminder.getPeriod().plusDate(loan.getDueDate()), - config - ); - - scheduledNoticesRepository.create(scheduledNotice); + ReminderConfig firstReminder = + loan.getOverdueFinePolicy().getRemindersPolicy().getFirstReminder(); + instantiateFirstScheduledNotice(records, firstReminder) + .thenAccept(r -> r.after(scheduledNoticesRepository::create)); } else { log.debug("The current item, barcode {}, is not subject to a reminder fees policy.", loan.getItem().getBarcode()); } return succeeded(records); } + + private CompletableFuture> instantiateFirstScheduledNotice( + LoanAndRelatedRecords loanRecords, ReminderConfig reminderConfig) { + + log.debug("instantiateFirstScheduledNotice:: parameters loanAndRelatedRecords: {}, " + + "reminderConfig: {}", loanRecords, reminderConfig); + + final Loan loan = loanRecords.getLoan(); + + return reminderConfig.nextNoticeDueOn(loan.getDueDate(), loanRecords.getTimeZone(), + loan.getCheckoutServicePointId(), calendarRepository) + .thenApply(r -> r.map(nextDueTime -> new ScheduledNotice(UUID.randomUUID().toString(), + loan.getId(), null, loan.getUserId(), null, null, + TriggeringEvent.DUE_DATE_WITH_REMINDER_FEE, nextDueTime, + instantiateNoticeConfig(reminderConfig)))); + } + + private ScheduledNoticeConfig instantiateNoticeConfig(ReminderConfig reminderConfig) { + return new ScheduledNoticeConfig(NoticeTiming.AFTER, null, + reminderConfig.getNoticeTemplateId(), reminderConfig.getNoticeFormat(), true); + } + } diff --git a/src/main/java/org/folio/circulation/domain/notice/schedule/ScheduledDigitalReminderHandler.java b/src/main/java/org/folio/circulation/domain/notice/schedule/ScheduledDigitalReminderHandler.java index 500d34ce59..b717856806 100644 --- a/src/main/java/org/folio/circulation/domain/notice/schedule/ScheduledDigitalReminderHandler.java +++ b/src/main/java/org/folio/circulation/domain/notice/schedule/ScheduledDigitalReminderHandler.java @@ -7,7 +7,9 @@ import org.folio.circulation.domain.FeeFineOwner; import org.folio.circulation.domain.Item; import org.folio.circulation.domain.Loan; -import org.folio.circulation.domain.policy.OverdueFinePolicyRemindersPolicy; +import org.folio.circulation.domain.policy.RemindersPolicy; +import org.folio.circulation.infrastructure.storage.CalendarRepository; +import org.folio.circulation.infrastructure.storage.ConfigurationRepository; import org.folio.circulation.infrastructure.storage.feesandfines.FeeFineOwnerRepository; import org.folio.circulation.infrastructure.storage.loans.LoanPolicyRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; @@ -22,15 +24,18 @@ import java.math.BigDecimal; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.UUID; import java.util.concurrent.CompletableFuture; import static java.util.Objects.isNull; +import static java.util.concurrent.CompletableFuture.completedFuture; import static org.folio.circulation.domain.ItemStatus.CLAIMED_RETURNED; import static org.folio.circulation.domain.ItemStatus.DECLARED_LOST; import static org.folio.circulation.domain.notice.TemplateContextUtil.createLoanNoticeContext; import static org.folio.circulation.domain.representations.ContributorsToNamesMapper.mapContributorNamesToJson; import static org.folio.circulation.support.results.Result.*; +import static org.folio.circulation.support.results.MappingFunctions.when; import static org.folio.circulation.support.results.ResultBinding.mapResult; import static org.folio.circulation.domain.notice.TemplateContextUtil.createFeeFineChargeNoticeContext; @@ -50,9 +55,13 @@ public class ScheduledDigitalReminderHandler extends LoanScheduledNoticeHandler private final LoanPolicyRepository loanPolicyRepository; private final OverdueFinePolicyRepository overdueFinePolicyRepository; private final FeeFineOwnerRepository feeFineOwnerRepository; + + private final CalendarRepository calendarRepository; + private final CollectionResourceClient accountsStorageClient; private final CollectionResourceClient feeFineActionsStorageClient; + private final ConfigurationRepository configurationRepository; static final String ACCOUNT_FEE_FINE_ID_VALUE = "6b830703-f828-4e38-a0bb-ee81deacbd03"; @@ -64,8 +73,10 @@ public class ScheduledDigitalReminderHandler extends LoanScheduledNoticeHandler public ScheduledDigitalReminderHandler(Clients clients, LoanRepository loanRepository) { super(clients, loanRepository); + configurationRepository = new ConfigurationRepository(clients); this.systemTime = ClockUtil.getZonedDateTime(); this.loanPolicyRepository = new LoanPolicyRepository(clients); + this.calendarRepository = new CalendarRepository(clients); this.overdueFinePolicyRepository = new OverdueFinePolicyRepository(clients); this.feeFineOwnerRepository = new FeeFineOwnerRepository(clients); this.accountsStorageClient = clients.accountsStorageClient(); @@ -77,16 +88,20 @@ public ScheduledDigitalReminderHandler(Clients clients, LoanRepository loanRepos @Override protected CompletableFuture> handleContext(ScheduledNoticeContext context) { final ScheduledNotice notice = context.getNotice(); - return ofAsync(context) .thenCompose(r -> r.after(this::fetchNoticeData)) + .thenCompose(r -> r.after(when(this::isOpenDay, this::processNotice, this::skip))) + .thenCompose(r -> handleResult(r, notice)) + .exceptionally(t -> handleException(t, notice)); + } + + private CompletableFuture> processNotice(ScheduledNoticeContext context) { + return ofAsync(context) .thenCompose(r -> r.after(this::persistAccount)) .thenCompose(r -> r.after(this::createFeeFineAction)) .thenCompose(r -> r.after(this::sendNotice)) .thenCompose(r -> r.after(this::updateLoan)) - .thenCompose(r -> r.after(this::updateNotice)) - .thenCompose(r -> handleResult(r, notice)) - .exceptionally(t -> handleException(t, notice)); + .thenCompose(r -> r.after(this::updateNotice)); } @Override @@ -107,11 +122,29 @@ protected CompletableFuture> fetchData(ScheduledN .thenCompose(r -> r.after(this::fetchTemplate)) .thenCompose(r -> r.after(this::fetchLoan)) .thenApply(r -> r.next(this::failWhenLoanHasNoNextReminderScheduled)) - .thenCompose(r -> r.after(this::buildAccountObject)); + .thenCompose(r -> r.after(this::instantiateReminderFeeAccount)); + } + + private CompletableFuture> isOpenDay(ScheduledNoticeContext noticeContext) { + String servicePointId = noticeContext.getLoan().getCheckoutServicePointId(); + return getSystemTimeInTenantsZone() + .thenCompose(tenantTime -> calendarRepository.lookupOpeningDays( + tenantTime.toLocalDate(), servicePointId)) + .thenApply(r -> r.map(days -> days.getRequestedDay().isOpen())); + } + + private CompletableFuture getSystemTimeInTenantsZone() { + return configurationRepository + .findTimeZoneConfiguration() + .thenApply(tenantTimeZone -> systemTime.withZoneSameInstant(tenantTimeZone.value())); + } + + private CompletableFuture> skip(ScheduledNoticeContext previousResult) { + return completedFuture(succeeded(previousResult.getNotice())); } protected Result failWhenLoanHasNoNextReminderScheduled(ScheduledNoticeContext context) { - OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry nextReminder = context.getLoan().getNextReminder(); + RemindersPolicy.ReminderConfig nextReminder = context.getLoan().getNextReminder(); return isNull(nextReminder) ? failed(new RecordNotFoundFailure("next scheduled reminder", "reminder-for-loan-"+context.getLoan().getId())) @@ -130,9 +163,9 @@ private CompletableFuture> updateLoan(ScheduledNo .thenApply(r -> r.map(v -> context)); } - private CompletableFuture> buildAccountObject(ScheduledNoticeContext context) { + private CompletableFuture> instantiateReminderFeeAccount(ScheduledNoticeContext context) { Loan loan = context.getLoan(); - OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry reminder = context.getLoan().getNextReminder(); + RemindersPolicy.ReminderConfig reminder = context.getLoan().getNextReminder(); if (isNoticeIrrelevant(context) || reminder.hasZeroFee()) { return ofAsync(() -> context); } @@ -170,7 +203,7 @@ private CompletableFuture> lookupFeeFineOwner(ScheduledNoti } private CompletableFuture> persistAccount(ScheduledNoticeContext context) { - OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry reminder = context.getLoan().getNextReminder(); + RemindersPolicy.ReminderConfig reminder = context.getLoan().getNextReminder(); if (isNoticeIrrelevant(context) || reminder.hasZeroFee()) { return ofAsync(() -> context); } @@ -179,7 +212,7 @@ private CompletableFuture> persistAccount(Schedul } private CompletableFuture> createFeeFineAction(ScheduledNoticeContext context) { - OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry reminder = context.getLoan().getNextReminder(); + RemindersPolicy.ReminderConfig reminder = context.getLoan().getNextReminder(); if (isNoticeIrrelevant(context) || reminder.hasZeroFee()) { return ofAsync(() -> context); } @@ -206,22 +239,39 @@ private CompletableFuture> createFeeFineAction(Sc */ @Override protected CompletableFuture> updateNotice(ScheduledNoticeContext context) { - OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry nextReminder = context.getLoan().getNextReminder(); + RemindersPolicy.ReminderConfig nextReminder = context.getLoan().getNextReminder(); if (nextReminder == null) { return deleteNotice(context.getNotice(), "no more reminders scheduled"); } else if (isNoticeIrrelevant(context)) { return deleteNotice(context.getNotice(), "further reminder notices became irrelevant"); } else { - ScheduledNotice nextReminderNotice = context.getNotice() - .withNextRunTime(nextReminder.getPeriod().plusDate(systemTime)); - nextReminderNotice.getConfiguration() - .setTemplateId(nextReminder.getNoticeTemplateId()) - .setFormat(nextReminder.getNoticeFormat()); - - return scheduledNoticesRepository.update(nextReminderNotice); + return findNextRuntimeAndBuildNotice(context, nextReminder) + .thenCompose(scheduledNoticeResult -> scheduledNoticesRepository.update(scheduledNoticeResult.value())); } } + protected CompletableFuture> findNextRuntimeAndBuildNotice( + ScheduledNoticeContext context, RemindersPolicy.ReminderConfig nextReminder) { + log.debug("buildNextNotice:: parameters notice context: {}, reminder config: {}", + context, nextReminder); + + return configurationRepository.findTimeZoneConfiguration() + .thenCompose(tenantTimeZone -> nextReminder.nextNoticeDueOn(systemTime, tenantTimeZone.value(), + context.getLoan().getCheckoutServicePointId(), calendarRepository)) + .thenApply(r -> r.map(nextRunTime -> buildNextNotice(context, nextReminder, nextRunTime))); + } + + private static ScheduledNotice buildNextNotice( + ScheduledNoticeContext context, RemindersPolicy.ReminderConfig nextReminder, + ZonedDateTime nextRunTimeResult) { + ScheduledNotice nextReminderNotice = context.getNotice() + .withNextRunTime(nextRunTimeResult.truncatedTo(ChronoUnit.HOURS)); + nextReminderNotice.getConfiguration() + .setTemplateId(nextReminder.getNoticeTemplateId()) + .setFormat(nextReminder.getNoticeFormat()); + return nextReminderNotice; + } + @Override protected boolean isNoticeIrrelevant(ScheduledNoticeContext context) { Loan loan = context.getLoan(); @@ -232,8 +282,12 @@ protected boolean isNoticeIrrelevant(ScheduledNoticeContext context) { protected JsonObject buildNoticeContextJson(ScheduledNoticeContext context) { Loan loan = context.getLoan(); if (loan.getNextReminder().hasZeroFee()) { + log.debug("buildNoticeContextJson: reminder without fee, notice context: {} ", + context); return createLoanNoticeContext(loan); } else { + log.debug("buildNoticeContextJson: reminder with reminder fee, notice context: {} ", + context); return createFeeFineChargeNoticeContext(context.getAccount(), loan, context.getChargeAction()); } } @@ -255,7 +309,6 @@ static class ReminderFeeAccount { static final String USER_ID = "userId"; static final String ITEM_ID = "itemId"; static final String DUE_DATE = "dueDate"; - static final String RETURNED_DATE = "returnedDate"; static final String PAYMENT_STATUS = "paymentStatus"; static final String STATUS = "status"; static final String CONTRIBUTORS = "contributors"; diff --git a/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicy.java b/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicy.java index e107d04d2e..4e7d2082e4 100644 --- a/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicy.java +++ b/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicy.java @@ -17,12 +17,12 @@ public class OverdueFinePolicy extends Policy { private final OverdueFinePolicyFineInfo fineInfo; private final OverdueFinePolicyLimitInfo limitInfo; - private final OverdueFinePolicyRemindersPolicy remindersPolicy; + private final RemindersPolicy remindersPolicy; private final Flags flags; private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private OverdueFinePolicy(String id, String name, OverdueFinePolicyFineInfo fineInfo, - OverdueFinePolicyLimitInfo limitInfo, OverdueFinePolicyRemindersPolicy remindersPolicy, Flags flags) { + OverdueFinePolicyLimitInfo limitInfo, RemindersPolicy remindersPolicy, Flags flags) { super(id, name); this.fineInfo = fineInfo; this.limitInfo = limitInfo; @@ -46,7 +46,7 @@ public static OverdueFinePolicy from(JsonObject json) { ), new OverdueFinePolicyLimitInfo(getBigDecimalProperty(json, "maxOverdueFine"), getBigDecimalProperty(json, "maxOverdueRecallFine")), - OverdueFinePolicyRemindersPolicy.from(getObjectProperty(json, "reminderFeesPolicy")), + new RemindersPolicy(getObjectProperty(json, "reminderFeesPolicy")), new Flags( getBooleanProperty(json, "gracePeriodRecall"), getBooleanProperty(json, "countClosed"), @@ -109,7 +109,7 @@ public boolean isReminderFeesPolicy() { return remindersPolicy.hasReminderSchedule(); } - public OverdueFinePolicyRemindersPolicy getRemindersPolicy() { + public RemindersPolicy getRemindersPolicy() { return remindersPolicy; } @@ -117,7 +117,7 @@ private static class UnknownOverdueFinePolicy extends OverdueFinePolicy { UnknownOverdueFinePolicy(String id) { super(id, null, new OverdueFinePolicyFineInfo(null, null, null, null), new OverdueFinePolicyLimitInfo(null, null), - OverdueFinePolicyRemindersPolicy.from(null), + new RemindersPolicy(null), new Flags(false, false, false)); } } diff --git a/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicyRemindersPolicy.java b/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicyRemindersPolicy.java deleted file mode 100644 index 32f21c24b5..0000000000 --- a/src/main/java/org/folio/circulation/domain/policy/OverdueFinePolicyRemindersPolicy.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.folio.circulation.domain.policy; - -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import lombok.Getter; -import org.apache.commons.lang.WordUtils; -import org.folio.circulation.domain.notice.NoticeFormat; - -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; - -import static org.folio.circulation.support.json.JsonPropertyFetcher.getArrayProperty; -import static org.folio.circulation.support.json.JsonPropertyFetcher.getBigDecimalProperty; - -public class OverdueFinePolicyRemindersPolicy { - - private final ReminderSequence sequence; - - public static OverdueFinePolicyRemindersPolicy from (JsonObject json) { - ReminderSequence sequence = - ReminderSequence.from(getArrayProperty(json, "reminderSchedule")); - return new OverdueFinePolicyRemindersPolicy(sequence); - } - - private OverdueFinePolicyRemindersPolicy(ReminderSequence sequence) { - this.sequence = sequence; - } - - public boolean hasReminderSchedule () { - return !sequence.isEmpty(); - } - - public ReminderSequence getReminderSchedule() { - return sequence; - } - - public ReminderSequenceEntry getReminderSequenceEntry(int reminderNumber) { - return sequence.getEntry(reminderNumber); - } - - public static class ReminderSequence { - private final Map reminderSequenceEntries; - - private ReminderSequence () { - reminderSequenceEntries = new HashMap<>(); - } - - /** - * Creates schedule of reminder entries ordered by sequence numbers starting with 1 (not zero) - * @param remindersArray JsonArray 'reminderSchedule' from the reminder fees policy - */ - public static ReminderSequence from (JsonArray remindersArray) { - ReminderSequence sequence = new ReminderSequence(); - for (int i = 1; i<=remindersArray.size(); i++) { - sequence.reminderSequenceEntries.put( - i, ReminderSequenceEntry.from(i, remindersArray.getJsonObject(i-1))); - } - return sequence; - } - - public boolean isEmpty() { - return reminderSequenceEntries.isEmpty(); - } - - public ReminderSequenceEntry getEntry(int sequenceNumber) { - if (reminderSequenceEntries.size() >= sequenceNumber) { - return reminderSequenceEntries.get(sequenceNumber); - } else { - return null; - } - } - - public boolean hasEntryAfter(int sequenceNumber) { - return reminderSequenceEntries.size() >= sequenceNumber+1; - } - - public ReminderSequenceEntry getEntryAfter(int sequenceNumber) { - return hasEntryAfter(sequenceNumber) ? getEntry(sequenceNumber+1) : null; - } - - } - - @Getter - public static class ReminderSequenceEntry { - private static final String INTERVAL = "interval"; - private static final String TIME_UNIT_ID = "timeUnitId"; - private static final String REMINDER_FEE = "reminderFee"; - private static final String NOTICE_FORMAT = "noticeFormat"; - private static final String NOTICE_TEMPLATE_ID = "noticeTemplateId"; - private static final String BLOCK_TEMPLATE_ID = "blockTemplateId"; - - private final int sequenceNumber; - private final Period period; - private final BigDecimal reminderFee; - private final String noticeFormat; - private final String noticeTemplateId; - private final String blockTemplateId; - - public ReminderSequenceEntry ( - int sequenceNumber, - Period period, - BigDecimal reminderFee, - String noticeFormat, - String noticeTemplateId, - String blockTemplateId) { - this.sequenceNumber = sequenceNumber; - this.period = period; - this.reminderFee = reminderFee; - this.noticeFormat = noticeFormat; - this.noticeTemplateId= noticeTemplateId; - this.blockTemplateId = blockTemplateId; - } - public static ReminderSequenceEntry from (int sequenceNumber, JsonObject entry) { - Period period = Period.from( - entry.getInteger(INTERVAL), - normalizeTimeUnit(entry.getString(TIME_UNIT_ID))); - BigDecimal fee = getBigDecimalProperty(entry,REMINDER_FEE); - return new ReminderSequenceEntry( - sequenceNumber, period, fee, - entry.getString(NOTICE_FORMAT), - entry.getString(NOTICE_TEMPLATE_ID), - entry.getString(BLOCK_TEMPLATE_ID)); - } - - public NoticeFormat getNoticeFormat () { - return NoticeFormat.from(noticeFormat); - } - - /** - * Normalizes "HOUR", "HOURS", "hour", "hours" to "Hours" - */ - private static String normalizeTimeUnit (String timeUnitId) { - String capitalized = WordUtils.capitalizeFully(timeUnitId); - return (capitalized.endsWith("s") ? capitalized : capitalized + "s"); - } - - public boolean hasZeroFee () { - return reminderFee.doubleValue() == 0.0; - } - - } -} diff --git a/src/main/java/org/folio/circulation/domain/policy/RemindersPolicy.java b/src/main/java/org/folio/circulation/domain/policy/RemindersPolicy.java new file mode 100644 index 0000000000..5b59840bd5 --- /dev/null +++ b/src/main/java/org/folio/circulation/domain/policy/RemindersPolicy.java @@ -0,0 +1,234 @@ +package org.folio.circulation.domain.policy; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import lombok.Getter; +import org.apache.commons.lang.WordUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.AdjacentOpeningDays; +import org.folio.circulation.domain.OpeningDay; +import org.folio.circulation.domain.notice.NoticeFormat; +import org.folio.circulation.infrastructure.storage.CalendarRepository; +import org.folio.circulation.support.http.server.ValidationError; +import org.folio.circulation.support.results.Result; + +import java.lang.invoke.MethodHandles; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + + +import static java.util.Collections.emptyMap; +import static org.folio.circulation.support.ValidationErrorFailure.singleValidationError; +import static org.folio.circulation.support.json.JsonPropertyFetcher.*; +import static org.folio.circulation.support.results.Result.*; +import static org.folio.circulation.support.results.Result.succeeded; + +@Getter +public class RemindersPolicy { + + public static final String REMINDER_SCHEDULE = "reminderSchedule"; + public static final String COUNT_CLOSED = "countClosed"; + public static final String IGNORE_GRACE_PERIOD_RECALL = "ignoreGracePeriodRecall"; + public static final String IGNORE_GRACE_PERIOD_HOLDS = "ignoreGracePeriodHolds"; + public static final String ALLOW_RENEWAL_OF_ITEMS_WITH_REMINDER_FEES = "allowRenewalOfItemsWithReminderFees"; + public static final String CLEAR_PATRON_BLOCK_WHEN_PAID = "clearPatronBlockWhenPaid"; + + private final Schedule schedule; + @Getter + private final Boolean countClosed; // Means "can make reminder due on closed day" + @Getter + private final Boolean ignoreGracePeriodRecall; + @Getter + private final Boolean ignoreGracePeriodHolds; + @Getter + private final Boolean allowRenewalOfItemsWithReminderFees; + @Getter + private final Boolean clearPatronBlockWhenPaid; + + public RemindersPolicy (JsonObject reminderFeesPolicy) { + this.schedule = new Schedule(this, getArrayProperty(reminderFeesPolicy, REMINDER_SCHEDULE)); + this.countClosed = getBooleanProperty(reminderFeesPolicy,COUNT_CLOSED); + this.ignoreGracePeriodRecall = getBooleanProperty(reminderFeesPolicy,ALLOW_RENEWAL_OF_ITEMS_WITH_REMINDER_FEES); + this.ignoreGracePeriodHolds = getBooleanProperty(reminderFeesPolicy,IGNORE_GRACE_PERIOD_RECALL); + this.allowRenewalOfItemsWithReminderFees = getBooleanProperty(reminderFeesPolicy,IGNORE_GRACE_PERIOD_HOLDS); + this.clearPatronBlockWhenPaid = getBooleanProperty(reminderFeesPolicy,CLEAR_PATRON_BLOCK_WHEN_PAID); + } + + public boolean canScheduleReminderUponClosedDay() { + return countClosed; + } + + public boolean hasReminderSchedule () { + return !schedule.isEmpty(); + } + + public ReminderConfig getFirstReminder () { + return schedule.getEntry(1); + } + + public ReminderConfig getNextReminderAfter(int sequenceNumber) { + return schedule.getEntryAfter(sequenceNumber); + } + + private static class Schedule { + + private final Map reminderSequenceEntries; + + /** + * Creates schedule of reminder entries ordered by sequence numbers starting with 1 (not zero) + * @param remindersArray JsonArray 'reminderSchedule' from the reminder fees policy + */ + private Schedule(RemindersPolicy policy, JsonArray remindersArray) { + reminderSequenceEntries = new HashMap<>(); + for (int i = 1; i<=remindersArray.size(); i++) { + reminderSequenceEntries.put( + i, new ReminderConfig(i, remindersArray.getJsonObject(i-1)).withPolicy(policy)); + } + } + + private boolean isEmpty() { + return reminderSequenceEntries.isEmpty(); + } + + private ReminderConfig getEntry(int sequenceNumber) { + if (reminderSequenceEntries.size() >= sequenceNumber) { + return reminderSequenceEntries.get(sequenceNumber); + } else { + return null; + } + } + + private boolean hasEntryAfter(int sequenceNumber) { + return reminderSequenceEntries.size() >= sequenceNumber+1; + } + + private ReminderConfig getEntryAfter(int sequenceNumber) { + return hasEntryAfter(sequenceNumber) ? getEntry(sequenceNumber+1) : null; + } + + } + + /** + * Represents single entry in a sequence of reminder configurations. + */ + @Getter + public static class ReminderConfig { + + final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + private static final String INTERVAL = "interval"; + private static final String TIME_UNIT_ID = "timeUnitId"; + private static final String REMINDER_FEE = "reminderFee"; + private static final String NOTICE_FORMAT = "noticeFormat"; + private static final String NOTICE_TEMPLATE_ID = "noticeTemplateId"; + private static final String BLOCK_TEMPLATE_ID = "blockTemplateId"; + + private final int sequenceNumber; + private final Period period; + private final BigDecimal reminderFee; + private final String noticeFormat; + private final String noticeTemplateId; + private final String blockTemplateId; + private RemindersPolicy policy; + + private ReminderConfig (int sequenceNumber, JsonObject entry) { + this.sequenceNumber = sequenceNumber; + this.period = Period.from(entry.getInteger(INTERVAL), + normalizeTimeUnit(entry.getString(TIME_UNIT_ID))); + this.reminderFee = getBigDecimalProperty(entry,REMINDER_FEE); + this.noticeFormat = entry.getString(NOTICE_FORMAT); + this.noticeTemplateId= entry.getString(NOTICE_TEMPLATE_ID); + this.blockTemplateId = entry.getString(BLOCK_TEMPLATE_ID); + } + + private ReminderConfig withPolicy(RemindersPolicy policy) { + this.policy = policy; + return this; + } + + public NoticeFormat getNoticeFormat () { + return NoticeFormat.from(noticeFormat); + } + + public boolean hasZeroFee () { + return reminderFee.doubleValue() == 0.0; + } + + /** + * Calculates when the next reminder will become due, potentially avoiding closed days depending on the closed days setting. + * Takes a date in UTC, returns the result in UTC, and retains the time part of the offset date. + * + * @param offsetDate The loan due date/time, or the date/time of the most recent reminder, in UTC + * @param tenantTimeZone The zone ID for checking the dates in the right timezone + * @param servicePointId For retrieving the calendar with open/closed days + * @param calendars Access to stored calendars + * @return The resulting date/time in UTC. + */ + public CompletableFuture> nextNoticeDueOn( + ZonedDateTime offsetDate, ZoneId tenantTimeZone, String servicePointId, CalendarRepository calendars) { + + log.debug("nextNoticeDueOn:: parameters offsetDate: {}, tenantTimeZone: {}" + + ", servicePointId: {}", offsetDate, tenantTimeZone, servicePointId ); + + ZonedDateTime scheduledForDateTime = getPeriod().plusDate(offsetDate); + if (policy.canScheduleReminderUponClosedDay()) { + log.debug("nextNoticeDueOn: ignoring closed days"); + return ofAsync(scheduledForDateTime); + } else { + log.debug("nextNoticeDueOn: taking closed days into account"); + return getFirstComingOpenDay(scheduledForDateTime, tenantTimeZone, servicePointId, calendars); + } + } + + private CompletableFuture> getFirstComingOpenDay( + ZonedDateTime scheduledDate, ZoneId tenantTimeZone, String servicePointId, CalendarRepository calendars) { + + log.debug("getFirstComingOpenDay:: parameters scheduledDate: {}, tenantTimeZone: {}" + + ", servicePointId: {}", scheduledDate, tenantTimeZone, servicePointId ); + + LocalDate scheduledDayInTenantTimeZone = scheduledDate.withZoneSameInstant(tenantTimeZone).toLocalDate(); + return calendars.lookupOpeningDays(scheduledDayInTenantTimeZone, servicePointId) + .thenApply(adjacentOpeningDaysResult -> daysUntilNextOpenDay(adjacentOpeningDaysResult.value())) + .thenCompose(daysUntilOpen -> ofAsync(scheduledDate.plusDays(daysUntilOpen.value()))); + } + + private Result daysUntilNextOpenDay(AdjacentOpeningDays openingDays) { + if (openingDays.getRequestedDay().isOpen()) { + return succeeded(0L); + } else { + OpeningDay nextDay = openingDays.getNextDay(); + if (!nextDay.isOpen()) { + return failed(singleValidationError( + new ValidationError("No calendar time table found for requested date", emptyMap()) + )); + } + return succeeded(ChronoUnit.DAYS.between(openingDays.getRequestedDay().getDate(), nextDay.getDate())); + } + } + + /** + * Normalizes "HOUR", "HOURS", "hour", "hours" to "Hours" + */ + private static String normalizeTimeUnit (String timeUnitId) { + String capitalized = WordUtils.capitalizeFully(timeUnitId); + return (capitalized.endsWith("s") ? capitalized : capitalized + "s"); + } + + public String toString() { + return "ReminderConfig{" + + "Sequence=#" + sequenceNumber + + ", " + period + + ", reminderFee=" + reminderFee + + ", noticeFormat=" + noticeFormat + + ", noticeTemplateId=" + noticeTemplateId + + ", blockTemplateId=" + blockTemplateId + "}"; + } + } +} diff --git a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java index bebe9c4c02..a4f804b70b 100644 --- a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java @@ -113,7 +113,7 @@ private void checkOut(RoutingContext routingContext) { final LoanScheduledNoticeService scheduledNoticeService = new LoanScheduledNoticeService(scheduledNoticesRepository, patronNoticePolicyRepository); final ReminderFeeScheduledNoticeService reminderFeeScheduledNoticesService = - new ReminderFeeScheduledNoticeService(scheduledNoticesRepository); + new ReminderFeeScheduledNoticeService(clients); OkapiPermissions permissions = OkapiPermissions.from(new WebContext(routingContext).getHeaders()); CirculationErrorHandler errorHandler = new OverridingErrorHandler(permissions); diff --git a/src/main/java/org/folio/circulation/resources/ScheduledDigitalRemindersProcessingResource.java b/src/main/java/org/folio/circulation/resources/ScheduledDigitalRemindersProcessingResource.java index 003730f75e..bf2873a67e 100644 --- a/src/main/java/org/folio/circulation/resources/ScheduledDigitalRemindersProcessingResource.java +++ b/src/main/java/org/folio/circulation/resources/ScheduledDigitalRemindersProcessingResource.java @@ -26,18 +26,19 @@ import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; import static org.folio.circulation.support.results.ResultBinding.mapResult; import static org.folio.circulation.support.utils.DateFormatUtil.formatDateTime; +import static org.folio.circulation.support.utils.LogUtil.multipleRecordsAsString; public class ScheduledDigitalRemindersProcessingResource extends ScheduledNoticeProcessingResource { protected static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); public ScheduledDigitalRemindersProcessingResource(HttpClient client) { super("/circulation/scheduled-digital-reminders-processing", client); - log.debug("Instantiating digital reminders processing - notices and fees"); + log.info("Instantiating digital reminders processing - notices and fees"); } @Override protected CompletableFuture>> findNoticesToSend(ConfigurationRepository configurationRepository, ScheduledNoticesRepository scheduledNoticesRepository, PatronActionSessionRepository patronActionSessionRepository, PageLimit pageLimit) { - return CqlQuery.lessThan("nextRunTime", formatDateTime(ClockUtil.getZonedDateTime().withZoneSameInstant(ZoneOffset.UTC))) + return CqlQuery.lessThanOrEqualTo("nextRunTime", formatDateTime(ClockUtil.getZonedDateTime().withZoneSameInstant(ZoneOffset.UTC))) .combine(exactMatch("noticeConfig.sendInRealTime", "true"), CqlQuery::and) .combine(exactMatch("triggeringEvent", DUE_DATE_WITH_REMINDER_FEE.getRepresentation()), CqlQuery::and) .combine(exactMatch("noticeConfig.format", "Email"), CqlQuery::and) @@ -47,6 +48,10 @@ protected CompletableFuture>> findNotice @Override protected CompletableFuture>> handleNotices(Clients clients, RequestRepository requestRepository, LoanRepository loanRepository, MultipleRecords noticesResult) { + + log.debug("handleNotices:: parameters noticesResult: {}", + () -> multipleRecordsAsString(noticesResult)); + return new ScheduledDigitalReminderHandler(clients, loanRepository) .handleNotices(noticesResult.getRecords()) .thenApply(mapResult(v -> noticesResult)); diff --git a/src/test/java/api/loans/CheckOutByBarcodeTests.java b/src/test/java/api/loans/CheckOutByBarcodeTests.java index 7637a1d664..fbd3c066e4 100644 --- a/src/test/java/api/loans/CheckOutByBarcodeTests.java +++ b/src/test/java/api/loans/CheckOutByBarcodeTests.java @@ -127,7 +127,6 @@ import api.support.builders.UserBuilder; import api.support.fakes.FakePubSub; import api.support.fakes.FakeStorageModule; -import api.support.http.CheckOutResource; import api.support.http.IndividualResource; import api.support.http.ItemResource; import api.support.http.OkapiHeaders; @@ -413,7 +412,7 @@ void canCheckOutUsingDueDateLimitedRollingLoanPolicy() { void canCheckOutUsingReminderFeePolicy() { IndividualResource loanPolicy = loanPoliciesFixture.canCirculateFixed(); - IndividualResource overdueFinePolicy = overdueFinePoliciesFixture.reminderFeesPolicy(); + IndividualResource overdueFinePolicy = overdueFinePoliciesFixture.remindersTwoDaysBetween(true); IndividualResource lostItemFeePolicy = lostItemFeePoliciesFixture.facultyStandard(); useFallbackPolicies(loanPolicy.getId(), diff --git a/src/test/java/api/loans/ReminderFeeTests.java b/src/test/java/api/loans/ReminderFeeTests.java index e60483e167..77bc741b13 100644 --- a/src/test/java/api/loans/ReminderFeeTests.java +++ b/src/test/java/api/loans/ReminderFeeTests.java @@ -1,10 +1,7 @@ package api.loans; import api.support.APITests; -import api.support.builders.CheckOutByBarcodeRequestBuilder; -import api.support.builders.FeeFineOwnerBuilder; -import api.support.builders.HoldingBuilder; -import api.support.builders.ItemBuilder; +import api.support.builders.*; import api.support.http.IndividualResource; import api.support.http.ItemResource; import api.support.http.UserResource; @@ -13,27 +10,41 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.LocalTime; import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.UUID; +import static api.support.fixtures.CalendarExamples.*; import static api.support.fixtures.ItemExamples.basedUponSmallAngryPlanet; import static api.support.utl.PatronNoticeTestHelper.*; +import static java.time.ZoneOffset.UTC; +import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.waitAtMost; import static org.folio.circulation.domain.representations.logs.LogEventType.NOTICE; import static org.folio.circulation.domain.representations.logs.LogEventType.NOTICE_ERROR; import static org.folio.circulation.support.utils.ClockUtil.getZonedDateTime; -import static org.folio.circulation.support.utils.DateTimeUtil.atEndOfDay; +import static org.folio.circulation.support.utils.DateFormatUtil.parseDateTime; +import static org.folio.circulation.support.utils.DateTimeUtil.atStartOfDay; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; class ReminderFeeTests extends APITests { private ItemResource item; private UserResource borrower; - private ZonedDateTime loanDate; + private UUID loanPolicyId; + private UUID requestPolicyId; + private UUID noticePolicyId; + private UUID lostItemFeePolicyId; + + private UUID remindersTwoDaysBetweenIncludeClosedDaysPolicyId; + private UUID remindersOneDayBetweenNotOnClosedDaysId; + + private UUID remindersTwoDaysBetweenNotOnClosedDaysPolicyId; @BeforeEach void beforeEach() { @@ -45,6 +56,7 @@ void beforeEach() { Collections.singletonList("CopyNumbers")); final UUID servicePointId = servicePointsFixture.cd1().getId(); + final IndividualResource homeLocation = locationsFixture.basedUponExampleLocation( item -> item.withPrimaryServicePoint(servicePointId)); @@ -71,31 +83,35 @@ void beforeEach() { templateFixture.createDummyNoticeTemplate(overdueFinePoliciesFixture.SECOND_REMINDER_TEMPLATE_ID); templateFixture.createDummyNoticeTemplate(overdueFinePoliciesFixture.THIRD_REMINDER_TEMPLATE_ID); - IndividualResource loanPolicy = loanPoliciesFixture.canCirculateFixed(); - IndividualResource overdueFinePolicy = overdueFinePoliciesFixture.reminderFeesPolicy(); - IndividualResource lostItemFeePolicy = lostItemFeePoliciesFixture.facultyStandard(); + remindersTwoDaysBetweenIncludeClosedDaysPolicyId = overdueFinePoliciesFixture + .remindersTwoDaysBetween(true).getId(); - useFallbackPolicies(loanPolicy.getId(), - requestPoliciesFixture.allowAllRequestPolicy().getId(), - noticePoliciesFixture.activeNotice().getId(), - overdueFinePolicy.getId(), - lostItemFeePolicy.getId()); + remindersOneDayBetweenNotOnClosedDaysId = overdueFinePoliciesFixture + .remindersOneDayBetween(false).getId(); - loanDate = atEndOfDay(getZonedDateTime()); + remindersTwoDaysBetweenNotOnClosedDaysPolicyId = overdueFinePoliciesFixture + .remindersTwoDaysBetween(false).getId(); - getZonedDateTime() - .withMonth(3) - .withDayOfMonth(18) - .withHour(11) - .withMinute(43) - .withSecond(54) - .truncatedTo(ChronoUnit.SECONDS); + + loanPolicyId = loanPoliciesFixture.canCirculateRolling().getId(); + lostItemFeePolicyId = lostItemFeePoliciesFixture.facultyStandard().getId(); + requestPolicyId = requestPoliciesFixture.allowAllRequestPolicy().getId(); + noticePolicyId = noticePoliciesFixture.activeNotice().getId(); + + loanDate = getZonedDateTime(); } @Test void checkOutWithReminderFeePolicyWillScheduleFirstReminder() { + useFallbackPolicies( + loanPolicyId, + requestPolicyId, + noticePolicyId, + remindersTwoDaysBetweenIncludeClosedDaysPolicyId, + lostItemFeePolicyId); + checkOutFixture.checkOutByBarcode( new CheckOutByBarcodeRequestBuilder() .forItem(item) @@ -107,51 +123,359 @@ void checkOutWithReminderFeePolicyWillScheduleFirstReminder() { } @Test - void willSendThreeRemindersAndCreateTwoAccountsThenStop() { + void willProcessRemindersAllDaysOpenAllowOnClosed() { + useFallbackPolicies( + loanPolicyId, + requestPolicyId, + noticePolicyId, + remindersTwoDaysBetweenIncludeClosedDaysPolicyId, + lostItemFeePolicyId); + + // Check out item, all days open service point final IndividualResource response = checkOutFixture.checkOutByBarcode( new CheckOutByBarcodeRequestBuilder() .forItem(item) .to(borrower) .on(loanDate) .at(servicePointsFixture.cd1())); + final JsonObject loan = response.getJson(); + ZonedDateTime dueDate = DateFormatUtil.parseDateTime(loan.getString("dueDate")); + + waitAtMost(1, SECONDS).until(scheduledNoticesClient::getAll, hasSize(1)); + + // Run scheduled reminder fee processing from the first day after due date + ZonedDateTime latestRunTime = dueDate.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + + // First processing + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after due date, don't send yet + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(0); + verifyNumberOfPublishedEvents(NOTICE, 0); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(0)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Second processing. Send. + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + // Two days after due date, send first + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Third processing, next reminder not yet due. + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Fourth processing (send). + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // Two days after latest reminder, send second. + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(2); + verifyNumberOfPublishedEvents(NOTICE, 2); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + // Second reminder has zero fee, don't create account + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Fifth processing (don't send yet). + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after second reminder, don't send yet + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(2); + verifyNumberOfPublishedEvents(NOTICE, 2); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Sixth processing (send now). + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // Two days after second reminder, send third and last + verifyNumberOfScheduledNotices(0); + verifyNumberOfSentNotices(3); + verifyNumberOfPublishedEvents(NOTICE, 3); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Seventh processing (no reminders to send). + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after third reminder, no scheduled reminder to send, no additional accounts + verifyNumberOfScheduledNotices(0); + verifyNumberOfSentNotices(3); + verifyNumberOfPublishedEvents(NOTICE, 3); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); + } + + @Test + void willProcessRemindersAllDaysOpenNoRemindersOnClosed() { + useFallbackPolicies( + loanPolicyId, + requestPolicyId, + noticePolicyId, + remindersTwoDaysBetweenNotOnClosedDaysPolicyId, + lostItemFeePolicyId); + + // Check out item, all days open service point + final IndividualResource response = checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .on(loanDate) + .at(servicePointsFixture.cd1())); final JsonObject loan = response.getJson(); + ZonedDateTime dueDate = DateFormatUtil.parseDateTime(loan.getString("dueDate")); + + waitAtMost(1, SECONDS).until(scheduledNoticesClient::getAll, hasSize(1)); + + // Run scheduled reminder fee processing from the first day after due date + ZonedDateTime latestRunTime = dueDate.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + + // First processing. + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after due date, don't send yet + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(0); + verifyNumberOfPublishedEvents(NOTICE, 0); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(0)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + // Second processing. Send. + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + // Two days after due date, send first + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // Two days after latest reminder, send second (has zero fee so no additional account) + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(2); + verifyNumberOfPublishedEvents(NOTICE, 2); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after second reminder, don't send yet + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(2); + verifyNumberOfPublishedEvents(NOTICE, 2); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // Two days after second reminder, send third and last + verifyNumberOfScheduledNotices(0); + verifyNumberOfSentNotices(3); + verifyNumberOfPublishedEvents(NOTICE, 3); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after third reminder, no scheduled reminder to send, no additional accounts + verifyNumberOfScheduledNotices(0); + verifyNumberOfSentNotices(3); + verifyNumberOfPublishedEvents(NOTICE, 3); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); + } + + @Test + void willScheduleRemindersAroundClosedDays() { + useFallbackPolicies( + loanPolicyId, + requestPolicyId, + noticePolicyId, + remindersOneDayBetweenNotOnClosedDaysId, + lostItemFeePolicyId); + + loanDate = atStartOfDay(FIRST_DAY.minusDays(1), UTC).plusHours(10).minusWeeks(3); + mockClockManagerToReturnFixedDateTime(loanDate); + JsonObject loan = checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .on(loanDate) + .at(CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN)).getJson(); ZonedDateTime dueDate = DateFormatUtil.parseDateTime(loan.getString("dueDate")); + assertThat(dueDate, is(ZonedDateTime.of(FIRST_DAY.minusDays(1), LocalTime.MIDNIGHT.minusHours(14), UTC))); + + waitAtMost(1, SECONDS).until(scheduledNoticesClient::getAll, hasSize(1)); + + ZonedDateTime latestRunTime = dueDate.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // Midnight after due date, don't send yet + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(0); + verifyNumberOfPublishedEvents(NOTICE, 0); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(0)); - ZonedDateTime firstRunTime = dueDate.plusMinutes(2); - scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(firstRunTime); + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); verifyNumberOfScheduledNotices(1); verifyNumberOfSentNotices(1); verifyNumberOfPublishedEvents(NOTICE, 1); verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(2); + verifyNumberOfPublishedEvents(NOTICE, 2); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + // Fee was zero, no additional account waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); - ZonedDateTime secondRunTime = dueDate.plusMinutes(4); - scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(secondRunTime); + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + verifyNumberOfScheduledNotices(0); + verifyNumberOfSentNotices(3); + verifyNumberOfPublishedEvents(NOTICE, 3); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); + + } + @Test + void willScheduleRemindersIgnoringClosedDays() { + useFallbackPolicies( + loanPolicyId, + requestPolicyId, + noticePolicyId, + remindersTwoDaysBetweenIncludeClosedDaysPolicyId, + lostItemFeePolicyId); + + loanDate = atStartOfDay(FIRST_DAY, UTC).plusHours(10).minusWeeks(3); + mockClockManagerToReturnFixedDateTime(loanDate); + JsonObject loan = checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .on(loanDate) + .at(CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN)).getJson(); + mockClockManagerToReturnDefaultDateTime(); + + assertThat(parseDateTime(loan.getString("dueDate")), + is(ZonedDateTime.of(FIRST_DAY, LocalTime.MIDNIGHT.minusHours(14), UTC))); + + ZonedDateTime dueDate = DateFormatUtil.parseDateTime(loan.getString("dueDate")); + + waitAtMost(1, SECONDS).until(scheduledNoticesClient::getAll, hasSize(1)); + + // Run scheduled reminder fee processing from the first day after due date + ZonedDateTime latestRunTime = dueDate.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after due date, don't send yet + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(0); + verifyNumberOfPublishedEvents(NOTICE, 0); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(0)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + // Two days after due date, send first + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // Two days after latest reminder, send second. + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(2); + verifyNumberOfPublishedEvents(NOTICE, 2); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + // Zero fee, no additional account + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + // One day after second reminder, don't send yet verifyNumberOfScheduledNotices(1); verifyNumberOfSentNotices(2); verifyNumberOfPublishedEvents(NOTICE, 2); verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); - // Second reminder has zero fee, don't create account waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); - ZonedDateTime thirdRunTime = dueDate.plusMinutes(6); - scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(thirdRunTime); + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + // Two days after second reminder, send third and last verifyNumberOfScheduledNotices(0); verifyNumberOfSentNotices(3); verifyNumberOfPublishedEvents(NOTICE, 3); verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); - ZonedDateTime fourthRunTime = dueDate.plusMinutes(8); - scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(fourthRunTime); + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + // One day after third reminder, no scheduled reminder to send, no additional accounts verifyNumberOfScheduledNotices(0); verifyNumberOfSentNotices(3); verifyNumberOfPublishedEvents(NOTICE, 3); @@ -159,9 +483,18 @@ void willSendThreeRemindersAndCreateTwoAccountsThenStop() { waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(2)); } + + @Test void willStopSendingRemindersCreatingAccountsAfterCheckIn() { + useFallbackPolicies( + loanPolicyId, + requestPolicyId, + noticePolicyId, + remindersTwoDaysBetweenIncludeClosedDaysPolicyId, + lostItemFeePolicyId); + final IndividualResource response = checkOutFixture.checkOutByBarcode( new CheckOutByBarcodeRequestBuilder() .forItem(item) @@ -172,9 +505,8 @@ void willStopSendingRemindersCreatingAccountsAfterCheckIn() { final JsonObject loan = response.getJson(); ZonedDateTime dueDate = DateFormatUtil.parseDateTime(loan.getString("dueDate")); - - ZonedDateTime firstRunTime = dueDate.plusMinutes(2); - scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(firstRunTime); + ZonedDateTime latestRunTime = dueDate.plusDays(2).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); verifyNumberOfScheduledNotices(1); verifyNumberOfSentNotices(1); @@ -185,8 +517,19 @@ void willStopSendingRemindersCreatingAccountsAfterCheckIn() { checkInFixture.checkInByBarcode(item); - ZonedDateTime secondRunTime = dueDate.plusMinutes(4); - scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(secondRunTime); + // After one day, the now obsolete reminder has not become due yet, nothing happens + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); + + verifyNumberOfScheduledNotices(1); + verifyNumberOfSentNotices(1); + verifyNumberOfPublishedEvents(NOTICE, 1); + verifyNumberOfPublishedEvents(NOTICE_ERROR, 0); + waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); + + // After two days, the obsolete reminder should be due, and thus be found and discarded + latestRunTime = latestRunTime.plusDays(1).truncatedTo(DAYS.toChronoUnit()).plusMinutes(1); + scheduledNoticeProcessingClient.runScheduledDigitalRemindersProcessing(latestRunTime); verifyNumberOfScheduledNotices(0); verifyNumberOfSentNotices(1); @@ -195,4 +538,5 @@ void willStopSendingRemindersCreatingAccountsAfterCheckIn() { waitAtMost(1, SECONDS).until(accountsClient::getAll, hasSize(1)); } + } diff --git a/src/test/java/api/support/builders/OverdueFinePolicyWithReminderFees.java b/src/test/java/api/support/builders/OverdueFinePolicyWithReminderFeesBuilder.java similarity index 76% rename from src/test/java/api/support/builders/OverdueFinePolicyWithReminderFees.java rename to src/test/java/api/support/builders/OverdueFinePolicyWithReminderFeesBuilder.java index 66e9c6fc1f..7820441f53 100644 --- a/src/test/java/api/support/builders/OverdueFinePolicyWithReminderFees.java +++ b/src/test/java/api/support/builders/OverdueFinePolicyWithReminderFeesBuilder.java @@ -4,7 +4,7 @@ import io.vertx.core.json.JsonObject; import java.util.UUID; -public class OverdueFinePolicyWithReminderFees implements Builder { +public class OverdueFinePolicyWithReminderFeesBuilder implements Builder { JsonObject overdueFinePolicyJson = new JsonObject(); public static final String ID = "id"; public static final String NAME = "name"; @@ -15,8 +15,9 @@ public class OverdueFinePolicyWithReminderFees implements Builder { public static final String REMINDER_FEE = "reminderFee"; public static final String NOTICE_FORMAT = "noticeFormat"; public static final String NOTICE_TEMPLATE_ID = "noticeTemplateId"; + public static final String COUNT_CLOSED = "countClosed"; - public OverdueFinePolicyWithReminderFees(UUID id, String name) { + public OverdueFinePolicyWithReminderFeesBuilder(UUID id, String name) { overdueFinePolicyJson .put(ID, id.toString()) .put(NAME, name) @@ -26,11 +27,17 @@ public OverdueFinePolicyWithReminderFees(UUID id, String name) { .put(REMINDER_SCHEDULE, new JsonArray()); } + JsonArray getReminderSchedule () { return overdueFinePolicyJson.getJsonObject(REMINDER_FEES_POLICY).getJsonArray(REMINDER_SCHEDULE); } - public OverdueFinePolicyWithReminderFees withAddedReminderEntry ( + public OverdueFinePolicyWithReminderFeesBuilder withCanSendReminderUponClosedDay(Boolean val) { + overdueFinePolicyJson.getJsonObject(REMINDER_FEES_POLICY).put(COUNT_CLOSED, val); + return this; + } + + public OverdueFinePolicyWithReminderFeesBuilder withAddedReminderEntry ( Integer interval, String timeUnitId, Double reminderFee, diff --git a/src/test/java/api/support/fixtures/CalendarExamples.java b/src/test/java/api/support/fixtures/CalendarExamples.java index 9c09a17b20..b4607cc08f 100644 --- a/src/test/java/api/support/fixtures/CalendarExamples.java +++ b/src/test/java/api/support/fixtures/CalendarExamples.java @@ -44,6 +44,7 @@ public class CalendarExamples { public static final String CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_CLOSED = "6ab38b7a-c889-4839-a337-86aad0297d7c"; public static final String CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN = "6ab38b7a-c889-4839-a337-86aad0297d8d"; + public static final String CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN = "309392b5-8ce8-4142-a983-b1162cab01aa"; public static final String CASE_MON_WED_FRI_OPEN_TUE_THU_CLOSED = "87291415-9f43-42ec-ba6b-d590f33509a0"; static final String CASE_START_DATE_MONTHS_AGO_AND_END_DATE_THU = "12345698-2f09-4bc9-8924-3734882d44a3"; @@ -87,6 +88,8 @@ public class CalendarExamples { public static final LocalDate THIRD_DAY_CLOSED = LocalDate.of(2020, 10, 31); public static final LocalDate THIRD_DAY_OPEN = LocalDate.of(2020, 10, 31); + public static final LocalDate FIRST_DAY = LocalDate.of(2023, 10, 29); + private static final String REQUESTED_DATE_PARAM = "date"; private static final Map fakeOpeningPeriods = new HashMap<>(); @@ -492,6 +495,42 @@ public static CalendarBuilder getCalendarById(String serviceId, MultiMap queries ) ) ); + case CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN: + LocalDate requestedDay = LocalDate.parse(queries.get(REQUESTED_DATE_PARAM)); + if (requestedDay.isEqual(FIRST_DAY)) { + return new CalendarBuilder( + new OpeningDayPeriodBuilder( + CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN, + new AdjacentOpeningDays( + new OpeningDay(new ArrayList<>(), requestedDay.minusDays(1), true, true), + new OpeningDay(new ArrayList<>(), requestedDay, true, false), + new OpeningDay(new ArrayList<>(), requestedDay.plusDays(1), true, true) + ) + ) + ); + } else if (requestedDay.isEqual(FIRST_DAY.plusDays(1))) { + return new CalendarBuilder( + new OpeningDayPeriodBuilder( + CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN, + new AdjacentOpeningDays( + new OpeningDay(new ArrayList<>(), requestedDay.minusDays(2), true, true), + new OpeningDay(new ArrayList<>(), requestedDay, true, true), + new OpeningDay(new ArrayList<>(), requestedDay.plusDays(1), true, true) + ) + ) + ); + } else { + return new CalendarBuilder( + new OpeningDayPeriodBuilder( + CASE_FIRST_DAY_CLOSED_FOLLOWING_OPEN, + new AdjacentOpeningDays( + new OpeningDay(new ArrayList<>(), requestedDay.minusDays(1), true, true), + new OpeningDay(new ArrayList<>(), requestedDay, true, true), + new OpeningDay(new ArrayList<>(), requestedDay.plusDays(1), true, true) + ) + ) + ); + } default: LocalDate requestedDate = LocalDate.parse(queries.get(REQUESTED_DATE_PARAM)); return new CalendarBuilder(buildAllDayOpenCalenderResponse(requestedDate, serviceId)); diff --git a/src/test/java/api/support/fixtures/OverdueFinePoliciesFixture.java b/src/test/java/api/support/fixtures/OverdueFinePoliciesFixture.java index 1c991c06df..3f83aa8c2d 100644 --- a/src/test/java/api/support/fixtures/OverdueFinePoliciesFixture.java +++ b/src/test/java/api/support/fixtures/OverdueFinePoliciesFixture.java @@ -5,7 +5,7 @@ import java.util.UUID; -import api.support.builders.OverdueFinePolicyWithReminderFees; +import api.support.builders.OverdueFinePolicyWithReminderFeesBuilder; import api.support.http.IndividualResource; import api.support.builders.NoticePolicyBuilder; @@ -104,17 +104,33 @@ public IndividualResource noOverdueFine() { return overdueFinePolicyRecordCreator.createIfAbsent(overdueFinePolicy); } - public IndividualResource reminderFeesPolicy() { - OverdueFinePolicyWithReminderFees policy = new OverdueFinePolicyWithReminderFees( + public IndividualResource remindersTwoDaysBetween(boolean canScheduleRemindersOnClosedDays) { + OverdueFinePolicyWithReminderFeesBuilder policy = new OverdueFinePolicyWithReminderFeesBuilder( UUID.randomUUID(), - "Overdue fine policy with scheduled reminder fees") + "Reminder fee policy 1-2-2: schedule on closed: " + canScheduleRemindersOnClosedDays) .withAddedReminderEntry( - 1,"minute",1.50, + 1,"day",1.50, "Email",FIRST_REMINDER_TEMPLATE_ID.toString()) - .withAddedReminderEntry(1, "minute", 0.00, + .withAddedReminderEntry(2, "day", 0.00, "Email", SECOND_REMINDER_TEMPLATE_ID.toString()) - .withAddedReminderEntry(1,"minute", 2.15, - "Email", THIRD_REMINDER_TEMPLATE_ID.toString()); + .withAddedReminderEntry(2,"day", 2.15, + "Email", THIRD_REMINDER_TEMPLATE_ID.toString()) + .withCanSendReminderUponClosedDay(canScheduleRemindersOnClosedDays); + return overdueFinePolicyRecordCreator.createIfAbsent(policy.create()); + } + + public IndividualResource remindersOneDayBetween(boolean canScheduleRemindersOnClosedDays) { + OverdueFinePolicyWithReminderFeesBuilder policy = new OverdueFinePolicyWithReminderFeesBuilder( + UUID.randomUUID(), + "Reminder fee policy 0-1-1: send on closed " + canScheduleRemindersOnClosedDays) + .withAddedReminderEntry( + 0,"day",1.50, + "Email",FIRST_REMINDER_TEMPLATE_ID.toString()) + .withAddedReminderEntry(1, "day", 0.00, + "Email", SECOND_REMINDER_TEMPLATE_ID.toString()) + .withAddedReminderEntry(1,"day", 2.15, + "Email", THIRD_REMINDER_TEMPLATE_ID.toString()) + .withCanSendReminderUponClosedDay(canScheduleRemindersOnClosedDays); return overdueFinePolicyRecordCreator.createIfAbsent(policy.create()); }