Close loan when lost item fees are closed

1 Problem overview

There is a requirement in CIRC-731 to close a declared lost loan once all the outstanding fee/fines is closed. These fees/fines are created automatically when an item is being declaring as lost. The fees/fines has Lost item fee and Lost item processing fee types (it is planed to also support actual cost of an item, so the list of types will be expanded, but now they are out of scope). 

The all fee/fine operations are owned by mod-feesfines service but loans are managed by mod-circulation. A naive approach to satisfy the requirement would be as following:

  1. Enhance PUT /accounts fee/fines endpoint:
    1. when an account has status.name == "Closed" and it is of the Lost item (processing) fee type and all other related fees for the loan is paid/closed
    2. then call mod-circulation to close the loan;
  2. Implement an API in mod-circulation to close a declared lost loan and mark item as Lost and paid.

The real problem here is on the 1.a step - mod-circulation already depends on mod-feesfines, and FOLIO does not support circular dependencies for now.

2 Possible solutions

  • Inversion of responsibilities: circulation handles accounts that have a loan associated;
  • Delegate business logic to UI;
  • Implement scheduled API to close the declared lost loans;
  • Use pub/sub mechanism.

3 Inversion of responsibilities approach overview

The idea of the approach in delegating some fee/fines activities for certain accounts (fee/fine records) to mod-circulation

3.1 Required UI changes 

UI have to implement the logic that decides which endpoint have to be executed to close the account. This logic have to be as much simple as possible, so we won't introduce strong coupling between mod-circulation, mod-feefines and UI. Suggested condition is following:

  • The account has a loan associated (loanId != null);
  • The account is being paid/closed (status.name = "Closed").

If all the above matched then do a call to the new circulation endpoint.

3.2 Required BE changes

New circulation API endpoint have to be implemented to handle payments for accounts with loans. This API should receive the account record. The logic of the API has to be following:

  1. Update the fee/fine account in the mod-feesfines DB;
  2. Fetch the loan, associated with the fee/fine account;
  3. Is the loan declared lost?
    1. No - exit the process;
    2. Yes - proceed;
  4. Fetch all open accounts, with feeFineId == ("<lost-item-fee-id>", "<lost-item-processing-fee-id>") ;
  5. Any account found?
    1. No - close the loan and mark item Lost and paid;
    2. Yes - exit the process 

3.3 Disadvantages of the approach 

  • Mod-circulation takes responsibility to manage fee/fines.
  • Account schema must be kept up to date in two modules.
  • Amount of requests to mod-circulation is going to be increased (assuming that it is already handles a lot of flows);
  • UI has to decide in which context the fee/fine record have to be updated (mod-feesfines or mod-circulation).

4 Delegate business logic to UI approach overview

The idea of this approach is to implement all the logic that decides whether or not to close a loan on UI side.

4.1 Required UI changes

The decision of closing an loan or not is owned by UI. The decision can be made by this rule:

  1. Update the fee/fine account as usual;
  2. If the fee/fine account matches all of the following:
    1. is closed
    2. balance = 0
    3. feeFineType = 'Lost item fee' or 'Lost item processing fee'
    4. loanId != null
  3. Fetch all the open accounts for the given loan (loanId == account.loanId) with feeFineType = 'Lost item fee' or 'Lost item processing fee';
    1. If any returned - do nothing;
    2. Otherwise - close the loan and update item status (send PUT /circulation/loans and PUT /inventory/items);

4.2 Required BE changes 

No changes required with current design, but some pieces can be implemented on BE side - for example an API endpoint to declare a loan as lost and paid, the API will be responsible for:

  1. Close a loan with/without a comment;
  2. Mark the loaned item with status Lost and paid.

4.3 Disadvantages of the approach 

  1. UI is responsible for handling business flows - any new consumers, that requires similar behavior, will have to implement this logic itself.
  2. A transaction can be terminated in a middle when:
    1. The user has some temp network issue;
    2. The user just closed browser or browser tab;
    3. etc.
  3. UI performance degradation.

5 Scheduled API to close declared lost loans approach overview

The idea of the approach is to implement a scheduled API (similar to loan anonymization or notices) that will be triggered once per a short interval (e.g. 5 mins) and will close declared lost loans that have no open accounts.

5.1 Required UI changes

No UI changes required.

5.2 Required BE changes

Implement new scheduled API /circulation/loans/scheduled-declared-lost-loans-resolving. The API have to be triggered once per some interval (have to be confirmed by a PO) and do the following:

  1. Fetch a chunk of loans (~1000 records) that matches following criteria:
    1. status.name == "Open";
    2. action == "declaredLost";
  2. If such loans found;
  3.  Then fetch open accounts for loans by following condition:
    1. loanId == (loan1.id, loan2.id, ..., loann.id);
    2. status.name =="Open";
    3. feeFineId == ("lost-item-fee-id", "lost-item-processing-fee-id");
  4. Associate loan and accounts;
  5. Any account found for a loan?
    1. Yes - nothing to do, loan can not be closed;
    2. No - close the loan and mark the item as Lost and paid.

5.3 Disadvantages of the approach 

  • The main disadvantage is high resources usage: the job will be executed once per some interval even if there are no declared lost loans or no accounts have been closed since last execution. In the first case at least one call to mod-circulation-storage will be sent and in the second case n/50 calls is sent (n - number of loans returned, 50 - number of IDs per a search - limitation to omit 414 error code);
  • The process is asynchronous, so end user won't be notified in case an error occurred when closing the loan. 

6 Use pub/sub approach overview

Mod-feesfines pushes an event when an account with loanId != null is closed, mod-circulation subscribes to the event topic, so it is being notified once a new event is published. 

6.1 Required UI changes

No UI changes required.

6.2 Required BE changes

6.2.1 Mod-feesfines required changes

Mod-feesfines have to publish an event when a an account is closed with a loan associated.

  1. Add dependency on mod-pubsub-client utility 

    pom.xml
    <dependency>
      <groupId>org.folio</groupId>
      <artifactId>mod-pubsub-client</artifactId>
      <version>x.y.z</version>
      <type>jar</type>
    </dependency>
  2. Add dependency to mod-pubsub API;
  3. Create MessagingDescriptor.json and place it in resources: 

    MessagingDescriptor.json
    {
      "publications": [
        {
          "eventType": "FF_ACCOUNT_WITH_LOAN_CLOSED",
          "description": "An account with a loan associated is closed",
          "eventTTL": 1,
          "signed": false
        }
      ],
      "subscriptions": []
    }
  4. Register module in pubsub on tenant init:

    TenantRefAPI.java
    PubSubClientUtils.registerModule(new OkapiConnectionParams(headers, vertx))
      .thenAccept(/* tenant-init-response*/)
  5. Add permissions needed for pubsub to the /_/tenant API: 

    ModuleDescriptor-template.json
    "modulePermissions": [
        "pubsub.event-types.post",
        "pubsub.publishers.post",
        "pubsub.subscribers.post"
    ]
  6. Push the event when an account was closed and has a loan associated (loanId != null && status.name == "Closed"):

    org.folio.rest.impl.AccountsAPI#putAccountsByAccountId
    PubSubClientUtils.sendEventMessage(new Event()
        .withId(UUID.randomUUID().toString())
        .withEventType("FF_ACCOUNT_WITH_LOAN_CLOSED")
        .withEventPayload(/*an payload with accountId, loanId, etc.*/)
        .withEventMetadata(new EventMetadata()
            .withPublishedBy(PubSubClientUtils.constructModuleName())
            .withTenantId(okapiHeaders.get(OKAPI_TENANT_HEADER))
            .withEventTTL(1)),
        new OkapiConnectionParams(okapiHeaders, vertxContext.owner()));

6.2.2 Mod-circulation required changes

Mod-circulation have to subscribe to this event and implement an API endpoint that will handle the event:

  1. Add dependency to the mod-pubsub-client utils;
  2. Add dependency on mod-pubsub API;
  3. Implement the TenantApi resource;
  4. Add the pubsub permissions to the /_/tenant API similar to mod-feesfines's;
  5. Add MessagingDescriptor.json: 

    MessagingDescriptor.json
    {
        "publications": [],
        "subscriptions": [
            {
                "eventType": "FF_ACCOUNT_WITH_LOAN_CLOSED",
                "callbackAddress": "/circulation/internal/handlers/close-declared-lost-loan-when-account-closed" // circulation API URI path. 
            }
        ]
    }
  6. Register module in pubsub on tenant init (the new TenantAPI.java class);
  7. Implement the /circulation/internal/handlers/close-declared-lost-loan-when-account-closed API, that will do following:
    1. Fetch loan, that specified in the request (Note: request is the payload specified in mod-feesfines on message push);
    2. If it is open and declared lost (has action == "declaredLost");
    3. Then fetch open accounts for the loan with feeFineId == ("<lost-item-fee-id>", "<lost-item-processing-fee-id>");
    4. If no accounts found, then loan can be closed.
  8. pub-sub user have to be granted to execute the API (should have the permission for the API).

6.3 Disadvantages of the approach 

  • Requires additional effort to set-up the messaging;
  • Dependency on the mod-pubsub service and it's availability (if it is not available then loans won't be closed);
  • The process is asynchronous, so end user won't be notified in case an error occurred when closing the loan.