Skip to content

Refactor fulfillment links logic - #476

Open
jazairi wants to merge 4 commits into
mainfrom
use-708
Open

jazairi wants to merge 4 commits into
mainfrom
use-708

Conversation

@jazairi

@jazairi jazairi commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Why these changes are being introduced:

The logic to resolve which fulfillment links to
display lives mostly in the view layer. As this
feature grows in complexity, the logic becomes
harder to understand, and bugs creep in. Most
recently, we noticed that two full-text options
links can appear for a given record.

A more idiomatic approach would be to shift the
logic to the model layer, thus making it more
visible, testable, and hopefully more
understandable. We also need to refactor the
Content Loader controller to make certain actions
explicit and guard against duplication.

Relevant ticket(s):

How this addresses that need:

  • Alma full-text options link actions are now made explicit in the Content Loader controller.
  • The Content Loader controller now checks for a Primo override action, which suppresses
    Primo links when they are not needed (e.g., a
    link is provided by Browzine/LibKey/etc).
  • Logic in the view layer is abstracted to a new Fulfillment Policy class.

Side effects of this change:

Complexity is not quite reduced as much as it is
redistributed to the new Fulfillment Policy class. However, the goal of this change is less to reduce complexity and more to improve understandability.

Developer

Accessibility
  • ANDI or WAVE has been run in accordance to our guide.
  • This PR contains no changes to the view layer.
  • New issues flagged by ANDI or WAVE have been resolved.
  • New issues flagged by ANDI or WAVE have been ticketed (link in the Pull Request details above).
  • No new accessibility issues have been flagged.
New ENV
  • All new ENV is documented in README.
  • All new ENV has been added to Heroku Pipeline, Staging and Prod.
  • ENV has not changed.
Approval beyond code review
  • UXWS/stakeholder approval has been confirmed.
  • UXWS/stakeholder review will be completed retroactively.
  • UXWS/stakeholder review is not needed.
Additional context needed to review

All expected fulfillment links, including 'full-text options', should appear for a given record with no duplicates. Since this is a refactor, I'd also like the reviewer's opinion on whether the new approach is an improvement (or not).

Code Reviewer

Code
  • I have confirmed that the code works as intended.
  • Any CodeClimate issues have been fixed or confirmed as
    added technical debt.
Documentation
  • The commit message is clear and follows our guidelines
    (not just this pull request message).
  • The documentation has been updated or is unnecessary.
  • New dependencies are appropriate or there were no changes.
Testing
  • There are appropriate tests covering any new functionality.
  • No additional test coverage is required.

@qltysh

qltysh Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

1 new issue

Tool Category Rule Count
qlty Structure Deeply nested control flow (level = 4) 1


fullTextOptionActions.forEach((action) => {
if (action !== preferredAction) {
action.remove()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deeply nested control flow (level = 4) [qlty:nested-control-flow]

Comment thread app/models/fulfillment_policy.rb Outdated
Comment thread app/models/fulfillment_policy.rb Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

OpenAlex and Alma actions are not fully included in the new override logic, allowing overlapping fallback links to remain visible.

3 open findings
What changed in this PR

Refactors fulfillment-link selection into a testable policy and adds explicit metadata for asynchronous link reconciliation.

Changes:

  • Introduces and tests FulfillmentPolicy.
  • Adds fulfillment source/type metadata to rendered links.
  • Updates content loading to suppress and deduplicate fallback actions.
File Description
app/​models/​fulfillment_policy.rb Centralizes Primo and Browzine link policy.
app/​javascript/​controllers/​content_loader_controller.js Reconciles asynchronously loaded actions.
app/​views/​search/​_result_primo.html.erb Uses the new policy.
app/​views/​alma/​sru.html.erb Identifies Alma actions.
app/​views/​openalex/​work.html.erb Identifies OpenAlex actions.
app/​views/​thirdiron/​libkey.html.erb Identifies LibKey override actions.
app/​views/​thirdiron/​browzine.html.erb Identifies Browzine actions.
test/​models/​fulfillment_policy_test.rb Tests policy behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/javascript/controllers/content_loader_controller.js Outdated
Comment thread app/views/openalex/work.html.erb
Comment thread app/views/openalex/work.html.erb
@mitlib
mitlib temporarily deployed to timdex-ui-pi-use-708-anozscfnk October 9, 2026 20:14 Inactive
@coveralls

coveralls commented Oct 9, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38652313555

Coverage increased (+0.03%) to 98.324%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 27 of 27 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1790
Covered Lines: 1760
Line Coverage: 98.32%
Coverage Strength: 81.37 hits per line

💛 - Coveralls

Why these changes are being introduced:

The logic to resolve which fulfillment links to
display lives mostly in the view layer. As this
feature grows in complexity, the logic becomes
harder to understand, and bugs creep in. Most
recently, we noticed that two full-text options
links can appear for a given record.

A more idiomatic approach would be to shift the
logic to the model layer, thus making it more
visible, testable, and hopefully more
understandable. We also need to refactor the
Content Loader controller to make certain actions
explicit and guard against duplication.

Relevant ticket(s):

- [USE-708](https://mitlibraries.atlassian.net/browse/USE-708)

How this addresses that need:

- Alma full-text options link actions are now
made explicit in the Content Loader controller.
- The Content Loader controller now checks for
a Primo override action, which suppresses
Primo links when they are not needed (e.g., a
link is provided by Browzine/LibKey/etc).
- Logic in the view layer is abstracted to a new
Fulfillment Policy class.

Side effects of this change:

Complexity is not quite reduced as much as it is
redistributed to the new Fulfillment Policy class.
However, the goal of this change is less to reduce
complexity and more to improve understandability.
… in override cleanup'

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@jazairi
jazairi temporarily deployed to timdex-ui-pi-use-708-anozscfnk October 9, 2026 20:34 Inactive
@JPrevost JPrevost self-assigned this Oct 13, 2026

@JPrevost JPrevost left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some clear documentation in the FulfillmentPolicy class to help me understand the intent? I think it might benefit from a rename to clarify it is just a PrimoFulfillmentPolicy class while doing that? If you disagree that is fine and just the added docs should be enough to allow me to more easily review this change.

Once the added docs are in place, I'll more confidently be able to proceed with the review.

Comment thread app/models/fulfillment_policy.rb Outdated
@@ -0,0 +1,72 @@
class FulfillmentPolicy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class and non-private method level documentation would be super helpful.

We are making this abstraction because this is complicated logic. The lack of docs here explaining what this class does or why in plain language is just moving the confusion for me.

Comment thread app/models/fulfillment_policy.rb Outdated
@result = result || {}
end

def primo_actions(record_link_enabled:)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are only methods for primo and browzine? Is this not (yet?) a FulfillmentPolicy class so much as a Primo/Browzine full text link class? I'm having a hard time understanding if all fulfillment links are affected by this policy logic and if so how (and if not, why not)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions. Yes, this policy is intentionally scoped to Primo, specifically the result[:links] object and whether Browzine emits a full-text options link. I suspect renaming the policy and adding better docs will help clarify this.

Comment thread app/views/search/_result_primo.html.erb Outdated
<% end %>
</div>

<%# Primo supplies various fulfillment links, which may overlap with LibKey/Browzine/etc. %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I fully understood that we didn't have any fulfillment links for non-Primo records at this time. I think we'll add some in the future. I wonder if it would be worthwhile to name the new policy a bit more specifically to ensure it is clear it is only for Primo Records and not Timdex records?

@jazairi
jazairi temporarily deployed to timdex-ui-pi-use-708-anozscfnk October 13, 2026 20:44 Inactive
Comment thread app/models/primo_fulfillment_policy.rb
Comment thread app/models/primo_fulfillment_policy.rb
Comment thread app/models/primo_fulfillment_policy.rb
Comment thread app/models/primo_fulfillment_policy.rb Outdated
Comment thread test/models/primo_fulfillment_policy_test.rb
Comment thread test/models/primo_fulfillment_policy_test.rb Outdated
@jazairi
jazairi temporarily deployed to timdex-ui-pi-use-708-anozscfnk October 13, 2026 20:46 Inactive
@jazairi
jazairi requested a review from JPrevost October 13, 2026 20:46
Comment thread app/models/primo_fulfillment_policy.rb Outdated

private

attr_reader :result

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. It's unclear to me why this is preferred over just using @result directly. I understand what it enables, but I feel like our code normally leans into the style of calling @result over attr_reader :result.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same question, actually. GPT convinced me that it's preferable to use a private reader rather than directly touching the instance variable everywhere. However, I think the fact that we were both thrown by this makes me wonder if the approach that's more idiomatic within our team is the better one, even if it isn't the optimal one.

const resultGet = resultContent.querySelector('.result-get')
if (almaFulltextOptions && resultGet) {
resultGet.appendChild(almaFulltextOptions)
const firstLibkeyActions = resultGet.querySelector('.libkey-actions')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firstLibkeyActions is named to clarify that Libkey Actions need to be displayed first and not that these are somehow the first LibKey Actions and more are coming later?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, that is ambiguous. It was intended to mean the first matching libkey-actions container in the DOM query. I think libkeyActionsContainer might be clearer.

Comment thread app/views/thirdiron/browzine.html.erb Outdated
<% if ThirdIron.enabled? && @browzine.present? && @browzine[:browzine_link].present? %>
<% if @full_record_url.present? %>
<%= link_to 'Full-text options', @full_record_url, class: 'button libkey-link', data: { matomo_seen: "Results, Full-text Options Link Seen, Tab: {{getActiveTabName}}", matomo_click: "Results, Full-text Options Link Engaged, Link: {{getElementText}}", content_piece: 'Full-text options' } %>
<%= link_to 'Full-text options', @full_record_url, class: 'button libkey-link', data: { matomo_seen: "Results, Full-text Options Link Seen, Tab: {{getActiveTabName}}", matomo_click: "Results, Full-text Options Link Engaged, Link: {{getElementText}}", content_piece: 'Full-text options', action_type: 'full_text_options', action_source: 'browzine', overrides_primo: true } %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How confident are we that this is the preferred full text options link?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question. It's probably not.

Back in June, the original full-text options implementation was to add a link to the full record if a record has Browzine links. That's how we ended up with the duplicate 'Full-text options' links, and this preserves the original behavior requested by UXWS.

That said, I'd imagine we'd prefer the SRU link, if available, since it goes directly to the full-text options section of the Primo record.

Comment thread app/views/thirdiron/libkey.html.erb Outdated
<% if @libkey[:pdf_link].present? || @libkey[:html_link].present? %>
<% if @libkey[:pdf_link].present? %>
<%= link_to( @libkey[:pdf_link][:text], @libkey[:pdf_link][:link], class: 'button libkey-link', data: { matomo_seen: "Results, LibKey Link Seen, Tab: {{getActiveTabName}}", matomo_click: "Results, LibKey Link Engaged, Link: {{getElementText}}", content_piece: @libkey[:pdf_link][:text] } ) %>
<%= link_to( @libkey[:pdf_link][:text], @libkey[:pdf_link][:link], class: 'button libkey-link', data: { matomo_seen: "Results, LibKey Link Seen, Tab: {{getActiveTabName}}", matomo_click: "Results, LibKey Link Engaged, Link: {{getElementText}}", content_piece: @libkey[:pdf_link][:text], action_type: 'full_text', action_source: 'libkey', overrides_primo: true } ) %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the affect of pdf_links and html_links both having the same action_type and action_source and both overriding Primo? Are we losing PDF or HTML or is this somehow retaining both?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The override refers specifically to 'full-text options' links. Basically, if we have a better fulfillment link to show (PDF and/or HTML), we show it instead of 'full-text options' or whatever might be included in result[:links]. If both PDF and HTML links are available, both would show. We're not suppressing them.

I'm wondering now, though, if I'm prematurely optimizing. I suspect we might get pushback for suppressing any links. Personally, I think it's good UX to streamline the CTA options, but I realize it's not my call to make.

@jazairi
jazairi temporarily deployed to timdex-ui-pi-use-708-anozscfnk October 14, 2026 23:06 Inactive
- Remove Primo link suppression
- Clarify docs
- Prefer full-text options coming from Alma or Primo during dedupe
- Use instance variable instead of attr_reader for clarity
@jazairi

jazairi commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@JPrevost Would you mind reviewing the latest commit before we merge? I know your comments were non-blocking, but they were thoughtful improvements, and I want to make sure we get this right. (Or, as right as we can, given what we currently know.)

Once this lands in staging, I'm planning to ask Darcy for QA just to be sure it's working the way UXWS expects.

This branch was successfully deployed

1 active deployment
timdex-ui-pi-use-708-anozscfnk — 9e04f2bb Deployed Oct 14, 2026 by jazairi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants