From 2ecbe54d2f0efb5949ff64a6df2b8902bd3ebc9a Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Mon, 15 Sep 2025 12:17:32 +0100 Subject: [PATCH] Bug 40808: Add Koha::Object classes for accountline_links and update existing objects This patch implements the Koha::Object layer for the new polymorphic account line linking system. New classes: - Koha::Account::Link - Individual link object with validation - Koha::Account::Links - Collection class with filtering methods Enhanced existing classes: - Koha::Account::Line gains methods: add_link, links, holds, checkouts, article_requests - Koha::Hold gains method: account_lines Features: - Clean API using modern class names (holds, checkouts vs reserves, issues) - Built-in validation for link integrity - Optimized queries with proper joins and prefetch - Seamless integration with existing Koha::Object patterns Usage examples: my $fees = $hold->debits; my $holds = $account_line->holds; $account_line->add_link('hold', $hold_id); --- Koha/Account/Line.pm | 99 +++++++++++++++++++++ Koha/Account/Link.pm | 173 +++++++++++++++++++++++++++++++++++++ Koha/Account/Links.pm | 141 ++++++++++++++++++++++++++++++ Koha/Exceptions/Account.pm | 16 ++++ Koha/Hold.pm | 18 ++++ 5 files changed, 447 insertions(+) create mode 100644 Koha/Account/Link.pm create mode 100644 Koha/Account/Links.pm diff --git a/Koha/Account/Line.pm b/Koha/Account/Line.pm index c796b92275a..862ad47568c 100644 --- a/Koha/Account/Line.pm +++ b/Koha/Account/Line.pm @@ -23,6 +23,8 @@ use C4::Log qw( logaction ); use Koha::Account::CreditType; use Koha::Account::DebitType; +use Koha::Account::Link; +use Koha::Account::Links; use Koha::Account::Offsets; use Koha::Database; use Koha::DateUtils qw( dt_from_string ); @@ -1100,6 +1102,103 @@ sub store { return $self->SUPER::store(); } +=head3 add_link + +Add a link to another entity (hold, checkout, article request, etc.) + + $account_line->add_link('hold', $hold_id); + +=cut + +sub add_link { + my ( $self, $link_type, $linked_id ) = @_; + + return Koha::Account::Link->new( + { + accountlines_id => $self->id, + link_type => $link_type, + linked_id => $linked_id, + } + )->store; +} + +=head3 links + +Get all links for this account line + + my $links = $account_line->links; + +=cut + +sub links { + my ($self) = @_; + + my $links_rs = $self->_result->search_related('accountline_links'); + return Koha::Account::Links->_new_from_dbic($links_rs); +} + +=head3 holds + +Get all holds linked to this account line + + my @holds = $account_line->holds; + +=cut + +sub holds { + my ($self) = @_; + + my @hold_ids = $self->links->search( { link_type => [ 'hold', 'old_hold' ] } )->get_column('linked_id'); + + return () unless @hold_ids; + + my @holds = Koha::Holds->search( { reserve_id => { -in => \@hold_ids } } )->as_list; + + my @old_holds = Koha::Old::Holds->search( { reserve_id => { -in => \@hold_ids } } )->as_list; + + return ( @holds, @old_holds ); +} + +=head3 checkouts + +Get all checkouts linked to this account line + + my @checkouts = $account_line->checkouts; + +=cut + +sub checkouts { + my ($self) = @_; + + my @checkout_ids = $self->links->search( { link_type => [ 'checkout', 'old_checkout' ] } )->get_column('linked_id'); + + return () unless @checkout_ids; + + my @checkouts = Koha::Checkouts->search( { issue_id => { -in => \@checkout_ids } } )->as_list; + + my @old_checkouts = Koha::Old::Checkouts->search( { issue_id => { -in => \@checkout_ids } } )->as_list; + + return ( @checkouts, @old_checkouts ); +} + +=head3 article_requests + +Get all article requests linked to this account line + + my @article_requests = $account_line->article_requests; + +=cut + +sub article_requests { + my ($self) = @_; + + my @ar_ids = $self->links->search( { link_type => 'article_request' } )->get_column('linked_id'); + + return () unless @ar_ids; + + return Koha::ArticleRequests->search( { id => { -in => \@ar_ids } } )->as_list; +} + =head2 Internal methods =cut diff --git a/Koha/Account/Link.pm b/Koha/Account/Link.pm new file mode 100644 index 00000000000..66611fb7c5e --- /dev/null +++ b/Koha/Account/Link.pm @@ -0,0 +1,173 @@ +package Koha::Account::Link; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Carp qw( croak ); +use Koha::Exceptions::Account; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::Account::Link - Koha Account Link Object class + +=head1 API + +=head2 Class methods + +=head3 store + +Validates the link before storing + +=cut + +sub store { + my ($self) = @_; + + my $link_type = $self->link_type; + my $linked_id = $self->linked_id; + + # Check for valid link type first + my $validators = { + 'hold' => sub { Koha::Holds->find( $_[0] ) }, + 'old_hold' => sub { Koha::Old::Holds->find( $_[0] ) }, + 'checkout' => sub { Koha::Checkouts->find( $_[0] ) }, + 'old_checkout' => sub { Koha::Old::Checkouts->find( $_[0] ) }, + 'article_request' => sub { Koha::ArticleRequests->find( $_[0] ) }, + }; + + unless ( exists $validators->{$link_type} ) { + Koha::Exceptions::Account::InvalidLinkType->throw("Unknown link_type: $link_type"); + } + + # Validate the linked entity exists + unless ( $self->_validate_link_integrity() ) { + Koha::Exceptions::Account::InvalidLinkTarget->throw( + "Invalid link target: " . $link_type . " with ID " . $linked_id . " does not exist" ); + } + + return $self->SUPER::store(); +} + +=head3 linked_object + +Returns the linked object based on link_type and linked_id + +=cut + +sub linked_object { + my ($self) = @_; + + return $self->_result->linked_object; +} + +=head3 linked_koha_object + +Returns the Koha::Object representation of the linked object + +=cut + +sub linked_koha_object { + my ($self) = @_; + + my $class_map = { + 'hold' => 'Koha::Hold', + 'old_hold' => 'Koha::Old::Hold', + 'checkout' => 'Koha::Checkout', + 'old_checkout' => 'Koha::Old::Checkout', + 'article_request' => 'Koha::ArticleRequest', + }; + + my $class = $class_map->{ $self->link_type }; + return unless $class; + + # Load the class if needed + eval "require $class"; + if ($@) { + Koha::Exceptions::Account::InvalidLinkType->throw( + "Could not load class $class for link_type '" . $self->link_type . "': $@" ); + } + + return $class->_new_from_dbic( $self->linked_object ); +} + +=head3 _validate_link_integrity + +Internal method to validate that the linked entity exists + +=cut + +sub _validate_link_integrity { + my ($self) = @_; + + my $link_type = $self->link_type; + my $linked_id = $self->linked_id; + + # Define valid link types and their validation methods + my $validators = { + 'hold' => sub { Koha::Holds->find( $_[0] ) }, + 'old_hold' => sub { Koha::Old::Holds->find( $_[0] ) }, + 'checkout' => sub { Koha::Checkouts->find( $_[0] ) }, + 'old_checkout' => sub { Koha::Old::Checkouts->find( $_[0] ) }, + 'article_request' => sub { Koha::ArticleRequests->find( $_[0] ) }, + }; + + my $validator = $validators->{$link_type}; + return 0 unless $validator; # Invalid link type + + return $validator->($linked_id) ? 1 : 0; +} + +=head2 Class Methods + +=head3 _type + +=cut + +sub _type { + return 'AccountlineLink'; +} + +=head1 AUTHOR + +Koha Development Team + +=head1 COPYRIGHT + +Copyright 2025 Koha Development Team + +=head1 LICENSE + +This file is part of Koha. + +Koha is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +Koha is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Koha; if not, see . + +=cut + +1; diff --git a/Koha/Account/Links.pm b/Koha/Account/Links.pm new file mode 100644 index 00000000000..b743970d85e --- /dev/null +++ b/Koha/Account/Links.pm @@ -0,0 +1,141 @@ +package Koha::Account::Links; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::Account::Links - Koha Account Links Object set class + +=head1 API + +=head2 Class methods + +=head3 by_link_type + +Filter links by link_type + + my $reserve_links = $links->by_link_type('reserve'); + +=cut + +sub by_link_type { + my ( $self, $link_type ) = @_; + + return $self->search( { link_type => $link_type } ); +} + +=head3 by_linked_id + +Filter links by linked_id + + my $links = $links->by_linked_id(12345); + +=cut + +sub by_linked_id { + my ( $self, $linked_id ) = @_; + + return $self->search( { linked_id => $linked_id } ); +} + +=head3 holds + +Get all holds linked to these account lines + +=cut + +sub holds { + my ($self) = @_; + + my @hold_ids = $self->search( { link_type => [ 'hold', 'old_hold' ] } )->get_column('linked_id'); + + return () unless @hold_ids; + + my @holds = Koha::Holds->search( { reserve_id => { -in => \@hold_ids } } )->as_list; + my @old_holds = Koha::Old::Holds->search( { reserve_id => { -in => \@hold_ids } } )->as_list; + + return ( @holds, @old_holds ); +} + +=head3 checkouts + +Get all checkouts linked to these account lines + +=cut + +sub checkouts { + my ($self) = @_; + + my @checkout_ids = $self->search( { link_type => [ 'checkout', 'old_checkout' ] } )->get_column('linked_id'); + + return () unless @checkout_ids; + + my @checkouts = Koha::Checkouts->search( { issue_id => { -in => \@checkout_ids } } )->as_list; + my @old_checkouts = Koha::Old::Checkouts->search( { issue_id => { -in => \@checkout_ids } } )->as_list; + + return ( @checkouts, @old_checkouts ); +} + +=head2 Class Methods + +=head3 _type + +=cut + +sub _type { + return 'AccountlineLink'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Account::Link'; +} + +=head1 AUTHOR + +Koha Development Team + +=head1 COPYRIGHT + +Copyright 2025 Koha Development Team + +=head1 LICENSE + +This file is part of Koha. + +Koha is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +Koha is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Koha; if not, see . + +=cut + +1; diff --git a/Koha/Exceptions/Account.pm b/Koha/Exceptions/Account.pm index 0b36f0818f9..16354c6b0e3 100644 --- a/Koha/Exceptions/Account.pm +++ b/Koha/Exceptions/Account.pm @@ -55,6 +55,14 @@ use Exception::Class ( 'Koha::Exceptions::Account::InvalidPaymentType' => { isa => 'Koha::Exceptions::Account', description => 'Invalid payment type' + }, + 'Koha::Exceptions::Account::InvalidLinkType' => { + isa => 'Koha::Exceptions::Account', + description => 'Invalid account line link type' + }, + 'Koha::Exceptions::Account::InvalidLinkTarget' => { + isa => 'Koha::Exceptions::Account', + description => 'Invalid account line link target' } ); @@ -98,6 +106,14 @@ Exception to be used when a passed credit or debit is not of a recognised type. Exception to be used when UseCashRegisters is enabled and one is not passed for a transaction. +=head2 Koha::Exceptions::Account::InvalidLinkType + +Exception to be used when an invalid link type is provided to account line linking. + +=head2 Koha::Exceptions::Account::InvalidLinkTarget + +Exception to be used when an account line link target does not exist or is invalid. + =cut 1; diff --git a/Koha/Hold.pm b/Koha/Hold.pm index 801d052fe82..f644475473c 100644 --- a/Koha/Hold.pm +++ b/Koha/Hold.pm @@ -1227,6 +1227,9 @@ sub charge_hold_fee { } ); + # Link the account line to this hold + $line->add_link( 'hold', $self->id ); + return $line; } @@ -1450,6 +1453,21 @@ sub strings_map { return $strings; } +=head3 debits + + my $fees = $hold->debits; + +Get all debit account lines (charges) associated with this hold + +=cut + +sub debits { + my ($self) = @_; + + my $accountlines_rs = $self->_result->search_related('accountline_links')->search_related('accountline'); + return Koha::Account::Debits->_new_from_dbic($accountlines_rs); +} + =head2 Internal methods =head3 _type -- 2.51.0