From ac7423d4843819446c72441940a90155dbf97b4c Mon Sep 17 00:00:00 2001 From: Jacob O'Mara Date: Wed, 21 Jan 2026 15:36:16 +0000 Subject: [PATCH] Bug 39658: API: Add REST endpoints for patron account links Endpoints: GET/POST/DELETE /patrons/{id}/account_links Signed-off-by: Trevor Diamond Signed-off-by: Kellie Stephens --- Koha/REST/V1/Patrons/AccountLinks.pm | 141 +++++++++++++++ .../definitions/patron_account_link.yaml | 19 ++ .../swagger/paths/patrons_account_links.yaml | 162 ++++++++++++++++++ api/v1/swagger/swagger.yaml | 6 + 4 files changed, 328 insertions(+) create mode 100644 Koha/REST/V1/Patrons/AccountLinks.pm create mode 100644 api/v1/swagger/definitions/patron_account_link.yaml create mode 100644 api/v1/swagger/paths/patrons_account_links.yaml diff --git a/Koha/REST/V1/Patrons/AccountLinks.pm b/Koha/REST/V1/Patrons/AccountLinks.pm new file mode 100644 index 0000000000..5f6a3a1baf --- /dev/null +++ b/Koha/REST/V1/Patrons/AccountLinks.pm @@ -0,0 +1,141 @@ +package Koha::REST::V1::Patrons::AccountLinks; + +# 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 Mojo::Base 'Mojolicious::Controller'; + +use Koha::Patrons; + +use Scalar::Util qw( blessed ); +use Try::Tiny qw( catch try ); + +=head1 NAME + +Koha::REST::V1::Patrons::AccountLinks + +=head1 API + +=head2 Methods + +=head3 list + +Controller function that handles listing Koha::Patron::AccountLink objects for the requested patron + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->param('patron_id') ); + + return $c->render_resource_not_found("Patron") + unless $patron; + + return try { + my $links_set = $patron->linked_account_links; + + return $c->render( status => 200, openapi => [] ) + unless $links_set; + + my $links = $c->objects->search($links_set); + return $c->render( status => 200, openapi => $links ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 add + +Controller function that handles linking a patron to another account + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->param('patron_id') ); + + return $c->render_resource_not_found("Patron") + unless $patron; + + my $body = $c->req->json; + my $linked_patron_id = $body->{linked_patron_id}; + my $linked_patron = Koha::Patrons->find($linked_patron_id); + + return $c->render_resource_not_found("Linked patron") + unless $linked_patron; + + return try { + my $link = $patron->link_to_patron($linked_patron); + + $c->res->headers->location( $c->req->url->to_string . '/' . $link->id ); + return $c->render( + status => 201, + openapi => $c->objects->to_api($link), + ); + } catch { + if ( blessed $_ ) { + if ( $_->isa('Koha::Exceptions::PatronAccountLink::AlreadyLinked') ) { + return $c->render( + status => 409, + openapi => { error => $_->description, error_code => 'AlreadyLinked' } + ); + } elsif ( $_->isa('Koha::Exceptions::PatronAccountLink::DifferentGroups') ) { + return $c->render( + status => 409, + openapi => { error => $_->description, error_code => 'DifferentGroups' } + ); + } + } + $c->unhandled_exception($_); + }; +} + +=head3 delete + +Controller method that handles removing a patron account link + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->param('patron_id') ); + + return $c->render_resource_not_found("Patron") + unless $patron; + + return try { + my $links_set = $patron->linked_account_links; + + return $c->render_resource_not_found("Account link") + unless $links_set; + + my $link = $links_set->find( $c->param('account_link_id') ); + + return $c->render_resource_not_found("Account link") + unless $link; + + $link->delete; + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/api/v1/swagger/definitions/patron_account_link.yaml b/api/v1/swagger/definitions/patron_account_link.yaml new file mode 100644 index 0000000000..d4a847cfbe --- /dev/null +++ b/api/v1/swagger/definitions/patron_account_link.yaml @@ -0,0 +1,19 @@ +--- +type: object +properties: + account_link_id: + type: integer + description: Internal link identifier + readOnly: true + link_group_id: + type: integer + description: Group identifier linking accounts together + patron_id: + type: integer + description: Patron this link belongs to + created_on: + type: string + format: date-time + description: When the link was created + readOnly: true +additionalProperties: false diff --git a/api/v1/swagger/paths/patrons_account_links.yaml b/api/v1/swagger/paths/patrons_account_links.yaml new file mode 100644 index 0000000000..8fd39534a4 --- /dev/null +++ b/api/v1/swagger/paths/patrons_account_links.yaml @@ -0,0 +1,162 @@ +--- +"/patrons/{patron_id}/account_links": + get: + x-mojo-to: Patrons::AccountLinks#list + operationId: listPatronAccountLinks + tags: + - patrons + summary: List linked accounts for a patron + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/patron_id_pp" + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + responses: + "200": + description: A list of linked patron accounts + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/patron_account_link" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + borrowers: edit_borrowers + post: + x-mojo-to: Patrons::AccountLinks#add + operationId: addPatronAccountLink + tags: + - patrons + summary: Link patron to another account + parameters: + - $ref: "../swagger.yaml#/parameters/patron_id_pp" + - name: body + in: body + description: A JSON object containing the patron_id to link to + required: true + schema: + type: object + properties: + linked_patron_id: + type: integer + description: The patron_id to link this patron to + required: + - linked_patron_id + produces: + - application/json + responses: + "201": + description: Link created + schema: + $ref: "../swagger.yaml#/definitions/patron_account_link" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict - patron already linked + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + borrowers: edit_borrowers +"/patrons/{patron_id}/account_links/{account_link_id}": + delete: + x-mojo-to: Patrons::AccountLinks#delete + operationId: deletePatronAccountLink + tags: + - patrons + summary: Remove a patron account link + parameters: + - $ref: "../swagger.yaml#/parameters/patron_id_pp" + - name: account_link_id + in: path + description: Internal account link identifier + required: true + type: integer + produces: + - application/json + responses: + "204": + description: Link deleted + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Link not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: Internal error + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + borrowers: edit_borrowers diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 9a9b7af756..db542c16b5 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -150,6 +150,8 @@ definitions: $ref: ./definitions/patron.yaml patron_account_credit: $ref: ./definitions/patron_account_credit.yaml + patron_account_link: + $ref: ./definitions/patron_account_link.yaml patron_balance: $ref: ./definitions/patron_balance.yaml patron_category: @@ -503,6 +505,10 @@ paths: $ref: "./paths/patrons_account.yaml#/~1patrons~1{patron_id}~1account~1debits~1{debit_id}" "/patrons/{patron_id}/account/debits": $ref: "./paths/patrons_account.yaml#/~1patrons~1{patron_id}~1account~1debits" + "/patrons/{patron_id}/account_links": + $ref: "./paths/patrons_account_links.yaml#/~1patrons~1{patron_id}~1account_links" + "/patrons/{patron_id}/account_links/{account_link_id}": + $ref: "./paths/patrons_account_links.yaml#/~1patrons~1{patron_id}~1account_links~1{account_link_id}" "/patrons/{patron_id}/extended_attributes": $ref: "./paths/patrons_extended_attributes.yaml#/~1patrons~1{patron_id}~1extended_attributes" "/patrons/{patron_id}/extended_attributes/{extended_attribute_id}": -- 2.39.5