From 4cf48ac6937331284390bb846bc41a733ad38458 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 - Koha::Hold gains method: account_lines Features: - Clean API using modern class names (holds) - 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 | 62 +++++++++++++ Koha/Account/Link.pm | 183 +++++++++++++++++++++++++++++++++++++ Koha/Account/Links.pm | 125 +++++++++++++++++++++++++ Koha/Exceptions/Account.pm | 16 ++++ Koha/Hold.pm | 18 ++++ 5 files changed, 404 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..77388ad4097 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,66 @@ 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) = @_; + + # Get all hold IDs - we only use 'hold' link_type for both current and old holds + my @hold_ids = $self->links->search( { link_type => 'hold' } )->get_column('linked_id'); + + return () unless @hold_ids; + + # Search both current holds and old holds with the same IDs + # The reserve_id is the same in both tables + 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 ); +} + =head2 Internal methods =cut diff --git a/Koha/Account/Link.pm b/Koha/Account/Link.pm new file mode 100644 index 00000000000..bd8dbf7e103 --- /dev/null +++ b/Koha/Account/Link.pm @@ -0,0 +1,183 @@ +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] ) || Koha::Old::Holds->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) = @_; + + # Special handling for holds - they can return either Koha::Hold or Koha::Old::Hold + if ( $self->link_type eq 'hold' ) { + my $dbic_object = $self->linked_object; + return unless $dbic_object; + + # Check which table the result came from by examining the result source + my $source_name = $dbic_object->result_source->source_name; + + if ( $source_name eq 'Reserve' ) { + require Koha::Hold; + return Koha::Hold->_new_from_dbic($dbic_object); + } elsif ( $source_name eq 'OldReserve' ) { + require Koha::Old::Hold; + return Koha::Old::Hold->_new_from_dbic($dbic_object); + } + + return; # Unknown source + } + + # Handle other link types normally + my $class_map = { + '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] ) || Koha::Old::Holds->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..0c470243ee2 --- /dev/null +++ b/Koha/Account/Links.pm @@ -0,0 +1,125 @@ +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) = @_; + + # Get all hold IDs - we only use 'hold' link_type for both current and old holds + my @hold_ids = $self->search( { link_type => 'hold' } )->get_column('linked_id'); + + return () unless @hold_ids; + + # Search both current holds and old holds with the same IDs + # The reserve_id is the same in both tables + 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 ); +} + +=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