diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 200c6a0750..98e4237619 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1060,7 +1060,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 * * *", @@ -1693,7 +1694,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.request-policy.get", "inventory-storage.items.item.put", - "circulation-item-storage.items.item.put", + "circulation-item.item.put", "circulation.internal.fetch-items", "users.item.get", "users.collection.get", @@ -1741,7 +1742,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.request-policy.get", "inventory-storage.items.item.put", - "circulation-item-storage.items.item.put", + "circulation-item.item.put", "circulation.internal.fetch-items", "users.item.get", "users.collection.get", @@ -2343,9 +2344,8 @@ "description" : "Internal permission set for fetching item(s)", "subPermissions": [ "inventory-storage.items.item.get", - "circulation-item-storage.item.collection.get", - "circulation-item-storage.items.collection.get", - "circulation-item-storage.items.item.get", + "circulation-item.collection.get", + "circulation-item.item.get", "inventory-storage.items.collection.get", "inventory-storage.holdings.item.get", "inventory-storage.holdings.collection.get", diff --git a/src/main/java/org/folio/circulation/domain/Item.java b/src/main/java/org/folio/circulation/domain/Item.java index 95e8ead468..d4fba931d6 100644 --- a/src/main/java/org/folio/circulation/domain/Item.java +++ b/src/main/java/org/folio/circulation/domain/Item.java @@ -12,6 +12,7 @@ import static org.folio.circulation.domain.representations.ItemProperties.STATUS_PROPERTY; import static org.folio.circulation.support.json.JsonPropertyFetcher.getBooleanProperty; import static org.folio.circulation.support.json.JsonPropertyFetcher.getNestedStringProperty; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; import static org.folio.circulation.support.json.JsonPropertyWriter.write; import java.util.Collection; @@ -399,7 +400,11 @@ public Item withInTransitDestinationServicePoint(ServicePoint servicePoint) { this.instance, this.materialType, this.loanType, this.description); } - public boolean isDcbItem(){ + public boolean isDcbItem() { return getBooleanProperty(itemRepresentation, "dcbItem"); } + + public String getLendingLibraryCode() { + return getProperty(itemRepresentation, "lendingLibraryCode"); + } } 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/TemplateContextUtil.java b/src/main/java/org/folio/circulation/domain/notice/TemplateContextUtil.java index 4b39d6b6de..92ab3145b5 100644 --- a/src/main/java/org/folio/circulation/domain/notice/TemplateContextUtil.java +++ b/src/main/java/org/folio/circulation/domain/notice/TemplateContextUtil.java @@ -219,7 +219,7 @@ private static JsonObject createItemContext(Item item) { .put("effectiveLocationSpecific", location.getName()) .put("effectiveLocationLibrary", location.getLibraryName()) .put("effectiveLocationCampus", location.getCampusName()) - .put("effectiveLocationInstitution", location.getInstitutionName()) + .put("effectiveLocationInstitution", item.isDcbItem()?item.getLendingLibraryCode():location.getInstitutionName()) .put("effectiveLocationDiscoveryDisplayName", location.getDiscoveryDisplayName()); var primaryServicePoint = location.getPrimaryServicePoint(); @@ -430,15 +430,11 @@ public UserContext withAddressProperties(JsonObject address) { } public String getCountryNameByCodeIgnoreCase(String code) { - if (StringUtils.isEmpty(code)) { + if (StringUtils.isEmpty(code) || !Stream.of(Locale.getISOCountries()).toList().contains(code)) { + log.info("getCountryNameByCodeIgnoreCase:: Invalid country code {}", code); return null; } - if (!Stream.of(Locale.getISOCountries()).toList().contains(code)) { - log.error("getCountryNameByCodeIgnoreCase:: Invalid country code {}", code); - throw new IllegalArgumentException("Not a valid country code to determine the country name."); - } - return new Locale("",code).getDisplayName(); } 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/infrastructure/storage/inventory/ItemRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java index 450aa9e8ec..de153ba0e2 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java @@ -23,7 +23,6 @@ import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.HashSet; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Function; @@ -64,7 +63,6 @@ public class ItemRepository { private final HoldingsRepository holdingsRepository; private final LoanTypeRepository loanTypeRepository; private final CollectionResourceClient circulationItemClient; - private final CollectionResourceClient circulationItemsByIdsClient; private final IdentityMap identityMap = new IdentityMap( item -> getProperty(item, "id")); @@ -74,8 +72,7 @@ public ItemRepository(Clients clients) { new MaterialTypeRepository(clients), new InstanceRepository(clients), new HoldingsRepository(clients.holdingsStorage()), new LoanTypeRepository(clients.loanTypesStorage()), - clients.circulationItemClient(), - clients.circulationItemsByIdsClient()); + clients.circulationItemClient()); } public CompletableFuture> fetchFor(ItemRelatedRecord itemRelatedRecord) { @@ -147,21 +144,13 @@ private CompletableFuture> getAvailableItem( } public CompletableFuture> fetchByBarcode(String barcode) { - return fetchItemByBarcode(barcode) + return fetchItemByBarcode(barcode, createItemFinder()) .thenComposeAsync(itemResult -> itemResult.after(when(item -> ofAsync(item::isNotFound), - item -> fetchCirculationItemByBarcode(barcode), item -> completedFuture(itemResult)))) + item -> fetchItemByBarcode(barcode, createCirculationItemFinder()) + , item -> completedFuture(itemResult)))) .thenComposeAsync(this::fetchItemRelatedRecords); } - private CompletableFuture> fetchCirculationItemByBarcode(String barcode) { - final var mapper = new ItemMapper(); - - return SingleRecordFetcher.jsonOrNull(circulationItemClient, "item") - .fetchWithQueryStringParameters(Map.of("barcode", barcode)) - .thenApply(mapResult(identityMap::add)) - .thenApply(r -> r.map(mapper::toDomain)); - } - public CompletableFuture> fetchById(String itemId) { return fetchItem(itemId) .thenComposeAsync(itemResult -> itemResult.after(when(item -> ofAsync(item::isNotFound), @@ -292,10 +281,9 @@ public CompletableFuture> fetchItemAsJson(String itemId) { .thenApply(mapResult(identityMap::add)); } - private CompletableFuture> fetchItemByBarcode(String barcode) { + private CompletableFuture> fetchItemByBarcode(String barcode, CqlQueryFinder finder) { log.info("Fetching item with barcode: {}", barcode); - final var finder = createItemFinder(); final var mapper = new ItemMapper(); return finder.findByQuery(exactMatch("barcode", barcode), one()) @@ -421,6 +409,6 @@ private CqlQueryFinder createItemFinder() { } private CqlQueryFinder createCirculationItemFinder() { - return new CqlQueryFinder<>(circulationItemsByIdsClient, "items", identity()); + return new CqlQueryFinder<>(circulationItemClient, "items", identity()); } } 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/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index 787dd9bf4c..3ffc41941a 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -68,7 +68,6 @@ public class Clients { private final CollectionResourceClient departmentClient; private final CollectionResourceClient checkOutLockStorageClient; private final CollectionResourceClient circulationItemClient; - private final CollectionResourceClient circulationItemsByIdsClient; private final GetManyRecordsClient settingsStorageClient; public static Clients create(WebContext context, HttpClient httpClient) { @@ -137,7 +136,6 @@ private Clients(OkapiHttpClient client, WebContext context) { checkOutLockStorageClient = createCheckoutLockClient(client, context); settingsStorageClient = createSettingsStorageClient(client, context); circulationItemClient = createCirculationItemClient(client, context); - circulationItemsByIdsClient = createCirculationItemsByIdsClient(client, context); } catch(MalformedURLException e) { throw new InvalidOkapiLocationException(context.getOkapiLocation(), e); @@ -376,10 +374,6 @@ public CollectionResourceClient circulationItemClient() { return circulationItemClient; } - public CollectionResourceClient circulationItemsByIdsClient() { - return circulationItemsByIdsClient; - } - private static CollectionResourceClient getCollectionResourceClient( OkapiHttpClient client, WebContext context, String path) @@ -807,12 +801,6 @@ private CollectionResourceClient createCirculationItemClient( return getCollectionResourceClient(client, context, "/circulation-item"); } - private CollectionResourceClient createCirculationItemsByIdsClient( - OkapiHttpClient client, WebContext context) throws MalformedURLException { - - return getCollectionResourceClient(client, context, "/circulation-item/items"); - } - private GetManyRecordsClient createSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/test/java/api/loans/CheckInByBarcodeTests.java b/src/test/java/api/loans/CheckInByBarcodeTests.java index 677b35694e..bac1af0d9d 100644 --- a/src/test/java/api/loans/CheckInByBarcodeTests.java +++ b/src/test/java/api/loans/CheckInByBarcodeTests.java @@ -420,6 +420,42 @@ void cannotCheckInWithoutACheckInDate() { "Checkin request must have an check in date"))); } + @Test + void canCheckInAnDcbItem() { + final UUID checkInServicePointId = servicePointsFixture.cd1().getId(); + IndividualResource instance = instancesFixture.basedUponDunkirk(); + IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); + IndividualResource locationsResource = locationsFixture.mainFloor(); + var barcode = "100002222"; + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem(barcode, holdings.getId(), locationsResource.getId()); + final CheckInByBarcodeResponse checkInResponse = checkInFixture.checkInByBarcode(circulationItem, ZonedDateTime.now(), checkInServicePointId); + + assertThat("Response should include an item", + checkInResponse.getJson().containsKey("item"), is(true)); + + final JsonObject itemFromResponse = checkInResponse.getItem(); + + assertThat("barcode is included for item", + itemFromResponse.getString("barcode"), is(barcode)); + } + + @Test + void slipContainsLendingLibraryCodeForDcb() { + final UUID checkInServicePointId = servicePointsFixture.cd1().getId(); + IndividualResource instance = instancesFixture.basedUponDunkirk(); + IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); + IndividualResource locationsResource = locationsFixture.mainFloor(); + var barcode = "100002222"; + var lendingLibraryCode = "11223"; + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItemWithLandingLibrary(barcode, holdings.getId(), locationsResource.getId(), lendingLibraryCode); + + final CheckInByBarcodeResponse checkInResponse = checkInFixture.checkInByBarcode(circulationItem, ZonedDateTime.now(), checkInServicePointId); + JsonObject staffSlipContext = checkInResponse.getStaffSlipContext(); + JsonObject itemContext = staffSlipContext.getJsonObject("item"); + + assertThat(itemContext.getString("effectiveLocationInstitution"), is(lendingLibraryCode)); + } + @Test void canCheckInAnItemWithoutAnOpenLoan() { final UUID checkInServicePointId = servicePointsFixture.cd1().getId(); 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/LoanAPITests.java b/src/test/java/api/loans/LoanAPITests.java index 02cd4fe31a..bc022d7b50 100644 --- a/src/test/java/api/loans/LoanAPITests.java +++ b/src/test/java/api/loans/LoanAPITests.java @@ -1289,7 +1289,7 @@ void loanInCollectionDoesProvideItemInformationForCirculationItem() { IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); IndividualResource locationsResource = locationsFixture.mainFloor(); - final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem(UUID.randomUUID(), "100002222", holdings.getId(), locationsResource.getId()); + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem("100002222", holdings.getId(), locationsResource.getId()); loansFixture.createLoan(circulationItem, usersFixture.jessica()); JsonObject loan = loansFixture.getLoans().getFirst(); 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/requests/StaffSlipsTests.java b/src/test/java/api/requests/StaffSlipsTests.java index a024e34b1f..22fb0cb198 100644 --- a/src/test/java/api/requests/StaffSlipsTests.java +++ b/src/test/java/api/requests/StaffSlipsTests.java @@ -42,6 +42,7 @@ import org.folio.circulation.support.utils.ClockUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; @@ -164,18 +165,33 @@ private static Collection getAllowedStatusesForHoldRequest() { } @ParameterizedTest - @EnumSource(value = SlipsType.class) - void responseContainsSlipWithAllAvailableTokens(SlipsType slipsType) { + @CsvSource({ + "US, false, PICK_SLIPS", + "US, false, SEARCH_SLIPS", + ", false, PICK_SLIPS", + ", false, SEARCH_SLIPS", + "XX, false, PICK_SLIPS", + "XX, false, SEARCH_SLIPS", + "US, true, PICK_SLIPS", + "US, true, SEARCH_SLIPS", + ", true, PICK_SLIPS", + ", true, SEARCH_SLIPS", + "XX, true, PICK_SLIPS", + "XX, true, SEARCH_SLIPS" + }) + void responseContainsSlipWithAllAvailableTokens(String countryCode, String primaryAddress, + String slipsTypeName) { + SlipsType slipsType = SlipsType.valueOf(slipsTypeName); IndividualResource servicePoint = servicePointsFixture.cd1(); UUID servicePointId = servicePoint.getId(); IndividualResource locationResource = locationsFixture.thirdFloor(); IndividualResource addressTypeResource = addressTypesFixture.home(); - Address address = AddressExamples.mainStreet(); + Address address = AddressExamples.mainStreet(countryCode); var departmentId1 = UUID.randomUUID().toString(); var departmentId2 = UUID.randomUUID().toString(); IndividualResource requesterResource = - usersFixture.steve(builder -> builder.withAddress(address).withDepartments( - new JsonArray(List.of(departmentId1, departmentId2)))); + usersFixture.steve(builder -> builder.withAddress(address).withDepartments(new JsonArray(List.of(departmentId1, departmentId2))) + .withPrimaryAddress(primaryAddress)); ZonedDateTime requestDate = ZonedDateTime.of(2019, 7, 22, 10, 22, 54, 0, UTC); final var requestExpiration = LocalDate.of(2019, 7, 30); final var holdShelfExpiration = LocalDate.of(2019, 8, 31); @@ -279,6 +295,10 @@ void responseContainsSlipWithAllAvailableTokens(SlipsType slipsType) { assertThat(requesterContext.getString("region"), is(address.getRegion())); assertThat(requesterContext.getString("postalCode"), is(address.getPostalCode())); assertThat(requesterContext.getString("countryId"), is(address.getCountryId())); + if(Boolean.valueOf(primaryAddress)) { + assertThat(requesterContext.getString("primaryCountry"), is((countryCode!=null && countryCode.equalsIgnoreCase("US")) ? + "United States" : null)); + } assertThat(requesterContext.getString("patronGroup"), is("Regular Group")); assertThat(requesterContext.getString("departments").split("; "), arrayContainingInAnyOrder(equalTo("test department1"),equalTo("test department2"))); diff --git a/src/test/java/api/support/builders/CirculationItemsBuilder.java b/src/test/java/api/support/builders/CirculationItemsBuilder.java index 1a22550b58..d3b5d6b84e 100644 --- a/src/test/java/api/support/builders/CirculationItemsBuilder.java +++ b/src/test/java/api/support/builders/CirculationItemsBuilder.java @@ -4,24 +4,145 @@ import java.util.UUID; -public class CirculationItemsBuilder implements Builder { +public class CirculationItemsBuilder extends JsonBuilder implements Builder { - private final JsonObject representation; + private final UUID itemId; + private final String barcode; + private final UUID holdingId; + private final UUID locationId; + private final UUID materialTypeId; + private final UUID loanTypeId; + private final boolean isDcb; + private final String lendingLibraryCode; - public CirculationItemsBuilder(UUID itemId, String barcode, UUID holdingId, UUID locationId, UUID materialTypeId, UUID loanTypeId, boolean isDcb) { + public CirculationItemsBuilder() { + this(UUID.randomUUID(), + "036000291452", + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + true, + "11223"); + } + + private CirculationItemsBuilder( + UUID itemId, + String barcode, + UUID holdingId, + UUID locationId, + UUID materialTypeId, + UUID loanTypeId, + boolean isDcb, + String lendingLibraryCode) { - this.representation = new JsonObject() - .put("id", itemId) - .put("holdingsRecordId", holdingId) - .put("effectiveLocationId", locationId) - .put("barcode", barcode) - .put("materialTypeId", materialTypeId) - .put("temporaryLoanTypeId", loanTypeId) - .put("dcbItem", isDcb); + this.itemId = itemId; + this.barcode = barcode; + this.holdingId = holdingId; + this.locationId = locationId; + this.materialTypeId = materialTypeId; + this.loanTypeId = loanTypeId; + this.isDcb = isDcb; + this.lendingLibraryCode = lendingLibraryCode; } - @Override public JsonObject create() { + JsonObject representation = new JsonObject(); + + representation.put("id", itemId); + representation.put("holdingsRecordId", holdingId); + representation.put("effectiveLocationId", locationId); + representation.put("barcode", barcode); + representation.put("materialTypeId", materialTypeId); + representation.put("temporaryLoanTypeId", loanTypeId); + representation.put("dcbItem", isDcb); + representation.put("lendingLibraryCode", lendingLibraryCode); + return representation; } + + public CirculationItemsBuilder withBarcode(String barcode) { + return new CirculationItemsBuilder( + this.itemId, + barcode, + this.holdingId, + this.locationId, + this.materialTypeId, + this.loanTypeId, + this.isDcb, + this.lendingLibraryCode); + } + + public CirculationItemsBuilder withHoldingId(UUID holdingId) { + return new CirculationItemsBuilder( + this.itemId, + this.barcode, + holdingId, + this.locationId, + this.materialTypeId, + this.loanTypeId, + this.isDcb, + this.lendingLibraryCode); + } + + public CirculationItemsBuilder withItemId(UUID itemId) { + return new CirculationItemsBuilder( + itemId, + this.barcode, + this.holdingId, + this.locationId, + this.materialTypeId, + this.loanTypeId, + this.isDcb, + this.lendingLibraryCode); + } + + public CirculationItemsBuilder withLocationId(UUID locationId) { + return new CirculationItemsBuilder( + this.itemId, + this.barcode, + this.holdingId, + locationId, + this.materialTypeId, + this.loanTypeId, + this.isDcb, + this.lendingLibraryCode); + } + + public CirculationItemsBuilder withLendingLibraryCode(String lendingLibraryCode) { + return new CirculationItemsBuilder( + this.itemId, + this.barcode, + this.holdingId, + this.locationId, + this.materialTypeId, + this.loanTypeId, + this.isDcb, + lendingLibraryCode); + } + + public CirculationItemsBuilder withLoanType(UUID loanTypeId) { + return new CirculationItemsBuilder( + this.itemId, + this.barcode, + this.holdingId, + this.locationId, + this.materialTypeId, + loanTypeId, + this.isDcb, + this.lendingLibraryCode); + } + + public CirculationItemsBuilder withMaterialType(UUID materialTypeId) { + return new CirculationItemsBuilder( + this.itemId, + this.barcode, + this.holdingId, + this.locationId, + materialTypeId, + this.loanTypeId, + this.isDcb, + this.lendingLibraryCode); + } + } 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/builders/UserBuilder.java b/src/test/java/api/support/builders/UserBuilder.java index 8eaa6281d7..a00ed4a112 100644 --- a/src/test/java/api/support/builders/UserBuilder.java +++ b/src/test/java/api/support/builders/UserBuilder.java @@ -23,9 +23,11 @@ public class UserBuilder extends JsonBuilder implements Builder { private final Collection
addresses; private final JsonArray departments; + private final boolean primaryAddress; + public UserBuilder() { this(UUID.randomUUID(), "sjones", "Jones", "Steven", null, null,"785493025613", - null, true, null, new ArrayList<>(), null); + null, true, null, new ArrayList<>(), null, false); } private UserBuilder( @@ -40,7 +42,8 @@ private UserBuilder( Boolean active, ZonedDateTime expirationDate, Collection
addresses, - JsonArray departments) { + JsonArray departments, + boolean primaryAddress) { this.id = id; this.username = username; @@ -57,6 +60,7 @@ private UserBuilder( this.addresses = addresses; this.departments = departments; + this.primaryAddress = primaryAddress; } public JsonObject create() { @@ -105,6 +109,7 @@ public JsonObject create() { put(mappedAddress, "region", address.getRegion()); put(mappedAddress, "postalCode", address.getPostalCode()); put(mappedAddress, "countryId", address.getCountryId()); + put(mappedAddress, "primaryAddress", this.primaryAddress); mappedAddresses.add(mappedAddress); }); @@ -134,7 +139,8 @@ public UserBuilder withName(String lastName, String firstName) { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withName(String lastName, String firstName, String middleName) { @@ -150,7 +156,8 @@ public UserBuilder withName(String lastName, String firstName, String middleName this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withPreferredFirstName(String lastName, String firstName,String preferredFirstName) { @@ -166,7 +173,8 @@ public UserBuilder withPreferredFirstName(String lastName, String firstName,Stri this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withPreferredFirstName(String lastName, String firstName,String middleName,String preferredFirstName) { @@ -182,7 +190,8 @@ public UserBuilder withPreferredFirstName(String lastName, String firstName,Stri this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withNoPersonalDetails() { @@ -198,7 +207,8 @@ public UserBuilder withNoPersonalDetails() { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withBarcode(String barcode) { @@ -214,7 +224,8 @@ public UserBuilder withBarcode(String barcode) { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withNoBarcode() { @@ -230,7 +241,8 @@ public UserBuilder withNoBarcode() { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withUsername(String username) { @@ -246,7 +258,8 @@ public UserBuilder withUsername(String username) { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder inGroupFor(IndividualResource patronGroup) { @@ -266,7 +279,8 @@ public UserBuilder withPatronGroupId(UUID patronGroupId) { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder active() { @@ -294,7 +308,8 @@ public UserBuilder withActive(Boolean active) { active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder expires(ZonedDateTime newExpirationDate) { @@ -310,7 +325,8 @@ public UserBuilder expires(ZonedDateTime newExpirationDate) { this.active, newExpirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder noExpiration() { @@ -326,7 +342,8 @@ public UserBuilder noExpiration() { this.active, null, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withAddress(Address address) { @@ -351,9 +368,26 @@ public UserBuilder withDepartments(JsonArray departments){ this.active, this.expirationDate, this.addresses, - departments); + departments, + this.primaryAddress); } + public UserBuilder withPrimaryAddress(String primaryAddress){ + return new UserBuilder( + this.id, + this.username, + this.lastName, + this.firstName, + this.middleName, + this.preferredFirstName, + this.barcode, + this.patronGroupId, + this.active, + this.expirationDate, + this.addresses, + departments, + Boolean.valueOf(primaryAddress)); + } public UserBuilder withNoAddresses() { return withAddresses(new ArrayList<>()); } @@ -371,7 +405,8 @@ private UserBuilder withAddresses(ArrayList
newAddresses) { this.active, this.expirationDate, newAddresses, - this.departments); + this.departments, + this.primaryAddress); } public UserBuilder withId(String id) { @@ -387,6 +422,7 @@ public UserBuilder withId(String id) { this.active, this.expirationDate, this.addresses, - this.departments); + this.departments, + this.primaryAddress); } } diff --git a/src/test/java/api/support/fakes/FakeOkapi.java b/src/test/java/api/support/fakes/FakeOkapi.java index cec33182ac..9cdfb40600 100644 --- a/src/test/java/api/support/fakes/FakeOkapi.java +++ b/src/test/java/api/support/fakes/FakeOkapi.java @@ -409,12 +409,6 @@ public void start(Promise startFuture) throws IOException { .withRecordConstraint(this::userHasAlreadyAcquiredLock) .create().register(router); - new FakeStorageModuleBuilder() - .withRootPath("/circulation-item/items") - .withCollectionPropertyName("items") - .withChangeMetadata() - .create().register(router); - new FakeStorageModuleBuilder() .withRootPath("/circulation-item") .withCollectionPropertyName("items") diff --git a/src/test/java/api/support/fixtures/AddressExamples.java b/src/test/java/api/support/fixtures/AddressExamples.java index 93228c27cd..b0cb61d5c2 100644 --- a/src/test/java/api/support/fixtures/AddressExamples.java +++ b/src/test/java/api/support/fixtures/AddressExamples.java @@ -21,11 +21,16 @@ public static Address RamkinResidence() { public static Address SiriusBlack() { return new Address(HOME_ADDRESS_TYPE, "12 Grimmauld Place", - null, "London", "London region", "123456", "GB"); + null, "London", "London region", "123456", "XX"); } public static Address mainStreet() { return new Address(HOME_ADDRESS_TYPE, "16 Main St", "Apt 3a", "Northampton", "MA", "01060", "US"); } + + public static Address mainStreet(String countryCode) { + return new Address(HOME_ADDRESS_TYPE, "16 Main St", + "Apt 3a", "Northampton", "MA", "01060", countryCode); + } } 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/CirculationItemsFixture.java b/src/test/java/api/support/fixtures/CirculationItemsFixture.java index 2d1c8320ac..3ff94568ca 100644 --- a/src/test/java/api/support/fixtures/CirculationItemsFixture.java +++ b/src/test/java/api/support/fixtures/CirculationItemsFixture.java @@ -7,7 +7,6 @@ import java.util.UUID; public class CirculationItemsFixture { - private final ResourceClient circulationItemsByIdsClient; private final ResourceClient circulationItemClient; private final MaterialTypesFixture materialTypesFixture; private final LoanTypesFixture loanTypesFixture; @@ -16,15 +15,22 @@ public CirculationItemsFixture( MaterialTypesFixture materialTypesFixture, LoanTypesFixture loanTypesFixture) { - circulationItemsByIdsClient = ResourceClient.forCirculationItemsByIds(); circulationItemClient = ResourceClient.forCirculationItem(); this.materialTypesFixture = materialTypesFixture; this.loanTypesFixture = loanTypesFixture; } - public IndividualResource createCirculationItem(UUID itemId, String barcode, UUID holdingId, UUID locationId) { - CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder(itemId, barcode, holdingId, locationId, materialTypesFixture.book().getId(), loanTypesFixture.canCirculate().getId(), true); - circulationItemClient.create(circulationItemsBuilder); - return circulationItemsByIdsClient.create(circulationItemsBuilder); + public IndividualResource createCirculationItem(String barcode, UUID holdingId, UUID locationId) { + CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder().withBarcode(barcode).withHoldingId(holdingId) + .withLoanType(loanTypesFixture.canCirculate().getId()).withMaterialType(materialTypesFixture.book().getId()) + .withLocationId(locationId); + return circulationItemClient.create(circulationItemsBuilder); + } + + public IndividualResource createCirculationItemWithLandingLibrary(String barcode, UUID holdingId, UUID locationId, String landingLibrary) { + CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder().withBarcode(barcode).withHoldingId(holdingId) + .withLoanType(loanTypesFixture.canCirculate().getId()).withMaterialType(materialTypesFixture.book().getId()) + .withLocationId(locationId).withLendingLibraryCode(landingLibrary); + return circulationItemClient.create(circulationItemsBuilder); } } 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()); } diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java index e8b5677d90..fedf39d696 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -44,10 +44,6 @@ public static URL itemsStorageUrl(String subPath) { return APITestContext.viaOkapiModuleUrl("/item-storage/items" + subPath); } - public static URL circulationItemsByIdsUrl(String subPath) { - return APITestContext.viaOkapiModuleUrl("/circulation-item/items" + subPath); - } - public static URL circulationItemUrl(String subPath) { return APITestContext.viaOkapiModuleUrl("/circulation-item" + subPath); } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index a07d01f1b6..558516054e 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -29,12 +29,8 @@ public static ResourceClient forItems() { return new ResourceClient(InterfaceUrls::itemsStorageUrl, "items"); } - public static ResourceClient forCirculationItemsByIds() { - return new ResourceClient(InterfaceUrls::circulationItemsByIdsUrl, "items"); - } - public static ResourceClient forCirculationItem() { - return new ResourceClient(InterfaceUrls::circulationItemUrl, "item"); + return new ResourceClient(InterfaceUrls::circulationItemUrl, "items"); } public static ResourceClient forHoldings() { diff --git a/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java b/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java index 9c3a076ebc..ce6afc117a 100644 --- a/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java +++ b/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java @@ -37,7 +37,7 @@ class ItemRepositoryTests { @Test void canUpdateAnItemThatHasBeenFetched() { final var itemsClient = mock(CollectionResourceClient.class); - final var repository = createRepository(itemsClient, null, null); + final var repository = createRepository(itemsClient, null); final var itemId = UUID.randomUUID().toString(); @@ -63,7 +63,7 @@ void canUpdateAnItemThatHasBeenFetched() { @Test void cannotUpdateAnItemThatHasNotBeenFetched() { - final var repository = createRepository(null, null, null); + final var repository = createRepository(null, null); final var notFetchedItem = dummyItem(); @@ -75,7 +75,7 @@ void cannotUpdateAnItemThatHasNotBeenFetched() { @Test void nullItemIsNotUpdated() { - final var repository = createRepository(null, null, null); + final var repository = createRepository(null, null); final var updateResult = get(repository.updateItem(null)); @@ -86,33 +86,35 @@ void nullItemIsNotUpdated() { @Test void returnCirculationItemWhenNotFound() { final var itemsClient = mock(CollectionResourceClient.class); - final var circulationItemsClient = mock(CollectionResourceClient.class); - final var itemId = UUID.randomUUID().toString(); - final var circulationItemsByIdsClient = mock(CollectionResourceClient.class); - final var repository = createRepository(itemsClient, circulationItemsClient, circulationItemsByIdsClient); + final var circulationItemClient = mock(CollectionResourceClient.class); + final var barcode = "HZFRKBNXIA"; + final var repository = createRepository(itemsClient, circulationItemClient); + + final var circulationItemJson = new JsonObject(); + circulationItemJson.put("id", "673bc784-6536-4286-a528-b0de544cf037"); + circulationItemJson.put("dcbItem", true); + circulationItemJson.put("barcode", barcode); + circulationItemJson.put("lendingLibraryCode", "123456"); + circulationItemJson.put("holdingsRecordId", "d0b9f1c8-8f3d-4c7e-8aef-5b9b5a7f9b7e"); - final var circulationItemJson = new JsonObject() - .put("id", itemId) - .put("holdingsRecordId", UUID.randomUUID()) - .put("effectiveLocationId", UUID.randomUUID()).toString(); final var emptyResult = new JsonObject() .put("items", new JsonArray()).toString(); + final var circulationItems = new JsonObject() + .put("items", new JsonArray().add(circulationItemJson)); when(itemsClient.getMany(any(), any())).thenReturn(ofAsync( () -> new Response(200, emptyResult, "application/json"))); - when(circulationItemsClient.getManyWithQueryStringParameters(any())).thenReturn(ofAsync( - () -> new Response(200, circulationItemJson, "application/json"))); - - assertThat(get(repository.fetchByBarcode(itemId)).value().getItemId(), is(itemId)); + when(circulationItemClient.getMany(any(), any())).thenReturn(ofAsync( + () -> new Response(200, circulationItems.toString(), "application/json"))); + assertThat(get(repository.fetchByBarcode(barcode)).value().getBarcode(), is(barcode)); } @Test void canUpdateCirculationItemThatHasBeenFetched(){ final var itemsClient = mock(CollectionResourceClient.class); - final var circulationItemsClient = mock(CollectionResourceClient.class); + final var circulationItemClient = mock(CollectionResourceClient.class); final var itemId = UUID.randomUUID().toString(); - final var circulationItemsByIdsClient = mock(CollectionResourceClient.class); - final var repository = createRepository(itemsClient, circulationItemsClient, circulationItemsByIdsClient); + final var repository = createRepository(itemsClient, circulationItemClient); final var circulationItemJson = new JsonObject() .put("id", itemId) @@ -125,7 +127,7 @@ void canUpdateCirculationItemThatHasBeenFetched(){ mockedClientGet(itemsClient, circulationItemJson.encodePrettily()); when(itemsClient.get(anyString())).thenReturn(ofAsync( () -> new Response(200, emptyResult, "application/json"))); - when(circulationItemsClient.put(any(), any())).thenReturn(ofAsync( + when(circulationItemClient.put(any(), any())).thenReturn(ofAsync( () -> new Response(204, circulationItemJson.toString(), "application/json"))); final var fetchedItem = get(repository.fetchById(itemId)).value(); @@ -139,7 +141,7 @@ private void mockedClientGet(CollectionResourceClient client, String body) { () -> new Response(200, body, "application/json"))); } - private ItemRepository createRepository(CollectionResourceClient itemsClient, CollectionResourceClient circulationItemClient, CollectionResourceClient circulationItemsByIdsClient) { + private ItemRepository createRepository(CollectionResourceClient itemsClient, CollectionResourceClient circulationItemClient) { final var locationRepository = mock(LocationRepository.class); final var materialTypeRepository = mock(MaterialTypeRepository.class); final var instanceRepository = mock(InstanceRepository.class); @@ -161,7 +163,7 @@ private ItemRepository createRepository(CollectionResourceClient itemsClient, Co return new ItemRepository(itemsClient, locationRepository, materialTypeRepository, instanceRepository, - holdingsRepository, loanTypeRepository, circulationItemClient, circulationItemsByIdsClient); + holdingsRepository, loanTypeRepository, circulationItemClient); } private Item dummyItem() {