Skip to content

Commit

Permalink
Circ 1920 reminders schedule around closed days (#1393)
Browse files Browse the repository at this point in the history
  • Loading branch information
nielserik authored Dec 7, 2023
1 parent cc810d2 commit fd6a0b9
Show file tree
Hide file tree
Showing 14 changed files with 820 additions and 257 deletions.
3 changes: 2 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 * * *",
Expand Down
11 changes: 5 additions & 6 deletions src/main/java/org/folio/circulation/domain/Loan.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<LoanAndRelatedRecords> 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<Result<ScheduledNotice>> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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";
Expand All @@ -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();
Expand All @@ -77,16 +88,20 @@ public ScheduledDigitalReminderHandler(Clients clients, LoanRepository loanRepos
@Override
protected CompletableFuture<Result<ScheduledNotice>> 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<Result<ScheduledNotice>> 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
Expand All @@ -107,11 +122,29 @@ protected CompletableFuture<Result<ScheduledNoticeContext>> 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<Result<Boolean>> 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<ZonedDateTime> getSystemTimeInTenantsZone() {
return configurationRepository
.findTimeZoneConfiguration()
.thenApply(tenantTimeZone -> systemTime.withZoneSameInstant(tenantTimeZone.value()));
}

private CompletableFuture<Result<ScheduledNotice>> skip(ScheduledNoticeContext previousResult) {
return completedFuture(succeeded(previousResult.getNotice()));
}

protected Result<ScheduledNoticeContext> 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()))
Expand All @@ -130,9 +163,9 @@ private CompletableFuture<Result<ScheduledNoticeContext>> updateLoan(ScheduledNo
.thenApply(r -> r.map(v -> context));
}

private CompletableFuture<Result<ScheduledNoticeContext>> buildAccountObject(ScheduledNoticeContext context) {
private CompletableFuture<Result<ScheduledNoticeContext>> 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);
}
Expand Down Expand Up @@ -170,7 +203,7 @@ private CompletableFuture<Result<FeeFineOwner>> lookupFeeFineOwner(ScheduledNoti
}

private CompletableFuture<Result<ScheduledNoticeContext>> persistAccount(ScheduledNoticeContext context) {
OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry reminder = context.getLoan().getNextReminder();
RemindersPolicy.ReminderConfig reminder = context.getLoan().getNextReminder();
if (isNoticeIrrelevant(context) || reminder.hasZeroFee()) {
return ofAsync(() -> context);
}
Expand All @@ -179,7 +212,7 @@ private CompletableFuture<Result<ScheduledNoticeContext>> persistAccount(Schedul
}

private CompletableFuture<Result<ScheduledNoticeContext>> createFeeFineAction(ScheduledNoticeContext context) {
OverdueFinePolicyRemindersPolicy.ReminderSequenceEntry reminder = context.getLoan().getNextReminder();
RemindersPolicy.ReminderConfig reminder = context.getLoan().getNextReminder();
if (isNoticeIrrelevant(context) || reminder.hasZeroFee()) {
return ofAsync(() -> context);
}
Expand All @@ -206,22 +239,39 @@ private CompletableFuture<Result<ScheduledNoticeContext>> createFeeFineAction(Sc
*/
@Override
protected CompletableFuture<Result<ScheduledNotice>> 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<Result<ScheduledNotice>> 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();
Expand All @@ -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());
}
}
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"),
Expand Down Expand Up @@ -109,15 +109,15 @@ public boolean isReminderFeesPolicy() {
return remindersPolicy.hasReminderSchedule();
}

public OverdueFinePolicyRemindersPolicy getRemindersPolicy() {
public RemindersPolicy getRemindersPolicy() {
return remindersPolicy;
}

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));
}
}
Expand Down
Loading

0 comments on commit fd6a0b9

Please sign in to comment.