From febe0f2378c654d079c5e7b8d90a788d0d27f7ad Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 29 May 2026 13:50:50 +0200 Subject: [PATCH 01/44] CROSSLINK-264 ask-retry action --- .../service/statemodels/returnables.json | 12 ++++++++++++ misc/returnables.yaml | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/broker/patron_request/service/statemodels/returnables.json b/broker/patron_request/service/statemodels/returnables.json index bb60c057..2e6b03a5 100644 --- a/broker/patron_request/service/statemodels/returnables.json +++ b/broker/patron_request/service/statemodels/returnables.json @@ -67,6 +67,11 @@ "name": "unfilled", "desc": "Supplier cannot supply (ISO18626 Unfilled)", "transition": "UNFILLED" + }, + { + "name": "accept-retry", + "desc": "Supplier accepts retry with new metadata", + "transition": "NEW" } ] }, @@ -338,6 +343,13 @@ "transitions": { "success": "CONDITION_PENDING" } + }, + { + "name": "ask-retry", + "desc": "Ask requester to retry with new ISO18626 metadata", + "transitions": { + "success": "NEW" + } } ], "events": [ diff --git a/misc/returnables.yaml b/misc/returnables.yaml index 606d0fd9..3e010e1d 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -48,6 +48,9 @@ states: - name: unfilled desc: Supplier cannot supply (ISO18626 Unfilled) transition: UNFILLED + - name: accept-retry + desc: Supplier accepts retry with new metadata + transition: NEW - name: SUPPLIER_LOCATED display: Supplier Located @@ -232,6 +235,10 @@ states: desc: Indicate will supply with conditions and send ISO18626 WillSupply transitions: success: CONDITION_PENDING + - name: ask-retry + desc: Ask requester to retry with new ISO18626 metadata + transitions: + success: NEW events: - name: cancel-request desc: Requester sent ISO18626 Cancel From ac9170a0a01b8d2dbb7e5beba08ba5ddaf8cf20a Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 29 May 2026 16:35:00 +0200 Subject: [PATCH 02/44] RETRY_REQUESTED state (yes, tests do not pass) --- .../patron_request/service/statemodels/returnables.json | 9 ++++++++- misc/returnables.yaml | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/broker/patron_request/service/statemodels/returnables.json b/broker/patron_request/service/statemodels/returnables.json index 2e6b03a5..179df8d4 100644 --- a/broker/patron_request/service/statemodels/returnables.json +++ b/broker/patron_request/service/statemodels/returnables.json @@ -348,7 +348,7 @@ "name": "ask-retry", "desc": "Ask requester to retry with new ISO18626 metadata", "transitions": { - "success": "NEW" + "success": "RETRY_REQUESTED" } } ], @@ -559,6 +559,13 @@ "desc": "After manual cannot-supply or automatically if auto-responder is on", "side": "SUPPLIER", "terminal": true + }, + { + "name": "RETRY_REQUESTED", + "display": "Retry Requested", + "desc": "After manual retry request", + "side": "SUPPLIER", + "terminal": true } ] } diff --git a/misc/returnables.yaml b/misc/returnables.yaml index 3e010e1d..072ed1e9 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -238,7 +238,7 @@ states: - name: ask-retry desc: Ask requester to retry with new ISO18626 metadata transitions: - success: NEW + success: RETRY_REQUESTED events: - name: cancel-request desc: Requester sent ISO18626 Cancel @@ -376,3 +376,9 @@ states: desc: After manual cannot-supply or automatically if auto-responder is on side: SUPPLIER terminal: true + + - name: RETRY_REQUESTED + display: Retry Requested + desc: After manual retry request + side: SUPPLIER + terminal: true From 67708e4240ff8a5e42797c8064f8a2fa06a2dc57 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 1 Jun 2026 14:29:39 +0200 Subject: [PATCH 03/44] Update state model for retry --- .../service/action_mapping_test.go | 5 +- .../service/statemodel_capabilities.go | 51 ++++++++++++----- .../service/statemodels/returnables.json | 57 ++++++++++++++++--- misc/returnables.yaml | 45 ++++++++++++--- 4 files changed, 129 insertions(+), 29 deletions(-) diff --git a/broker/patron_request/service/action_mapping_test.go b/broker/patron_request/service/action_mapping_test.go index 1475972e..acf755df 100644 --- a/broker/patron_request/service/action_mapping_test.go +++ b/broker/patron_request/service/action_mapping_test.go @@ -22,11 +22,12 @@ func TestNewReturnableActionMapping(t *testing.T) { BorrowerStateReceived: {{actionName: BorrowerActionCheckOut}}, BorrowerStateCheckedOut: {{actionName: BorrowerActionCheckIn}}, BorrowerStateCheckedIn: {{actionName: BorrowerActionShipReturn}}, + BorrowerStateRetryPending: {{actionName: BorrowerActionAcceptRetry}, {actionName: BorrowerActionRejectRetry}}, } lenderStateActionMapping := map[pr_db.PatronRequestState][]PatronRequestAction{ LenderStateNew: {{actionName: LenderActionValidate, auto: true}}, - LenderStateValidated: {{actionName: LenderActionWillSupply, auto: true}, {actionName: LenderActionCannotSupply}, {actionName: LenderActionAddCondition}}, + LenderStateValidated: {{actionName: LenderActionWillSupply, auto: true}, {actionName: LenderActionCannotSupply}, {actionName: LenderActionAddCondition}, {actionName: LenderActionAskRetry}}, LenderStateWillSupply: {{actionName: LenderActionAddCondition}, {actionName: LenderActionShip}, {actionName: LenderActionCannotSupply}}, LenderStateConditionPending: {{actionName: LenderActionAddCondition}, {actionName: LenderActionCannotSupply}}, LenderStateConditionAccepted: {{actionName: LenderActionAddCondition}, {actionName: LenderActionShip}, {actionName: LenderActionCannotSupply}}, @@ -165,7 +166,7 @@ func mapCompare(t *testing.T, map1 map[pr_db.PatronRequestState][]PatronRequestA for stateName := range map1 { listOne := map1[stateName] listTwo := map2[stateName] - assert.Equal(t, len(listOne), len(listTwo)) + assert.Equal(t, len(listOne), len(listTwo), "State %s has different number of actions in the two maps", stateName) for i := range listOne { assert.Equal(t, listOne[i].actionName, listTwo[i].actionName) assert.Equal(t, listOne[i].auto, listTwo[i].auto) diff --git a/broker/patron_request/service/statemodel_capabilities.go b/broker/patron_request/service/statemodel_capabilities.go index 0de18d58..62fc33d7 100644 --- a/broker/patron_request/service/statemodel_capabilities.go +++ b/broker/patron_request/service/statemodel_capabilities.go @@ -36,6 +36,9 @@ const ( BorrowerStateCompleted pr_db.PatronRequestState = "COMPLETED" BorrowerStateCancelled pr_db.PatronRequestState = "CANCELLED" BorrowerStateUnfilled pr_db.PatronRequestState = "UNFILLED" + BorrowerStateRetryPending pr_db.PatronRequestState = "RETRY_PENDING" + BorrowerStateRetryAccepted pr_db.PatronRequestState = "RETRY_ACCEPTED" + BorrowerStateRetryRejected pr_db.PatronRequestState = "RETRY_REJECTED" LenderStateNew pr_db.PatronRequestState = "NEW" LenderStateValidated pr_db.PatronRequestState = "VALIDATED" LenderStateWillSupply pr_db.PatronRequestState = "WILL_SUPPLY" @@ -48,6 +51,7 @@ const ( LenderStateCompleted pr_db.PatronRequestState = "COMPLETED" LenderStateCancelled pr_db.PatronRequestState = "CANCELLED" LenderStateUnfilled pr_db.PatronRequestState = "UNFILLED" + LenderStateCompletedWithRetry pr_db.PatronRequestState = "COMPLETED_WITH_RETRY" ) const ( @@ -60,6 +64,8 @@ const ( BorrowerActionCheckOut pr_db.PatronRequestAction = "check-out" BorrowerActionCheckIn pr_db.PatronRequestAction = "check-in" BorrowerActionShipReturn pr_db.PatronRequestAction = "ship-return" + BorrowerActionAcceptRetry pr_db.PatronRequestAction = "accept-retry" + BorrowerActionRejectRetry pr_db.PatronRequestAction = "reject-retry" LenderActionValidate pr_db.PatronRequestAction = "validate" LenderActionWillSupply pr_db.PatronRequestAction = "will-supply" @@ -69,22 +75,24 @@ const ( LenderActionShip pr_db.PatronRequestAction = "ship" LenderActionMarkReceived pr_db.PatronRequestAction = "mark-received" LenderActionAcceptCancel pr_db.PatronRequestAction = "accept-cancel" + LenderActionAskRetry pr_db.PatronRequestAction = "ask-retry" ) const ( - SupplierExpectToSupply MessageEvent = "expect-to-supply" - SupplierWillSupply MessageEvent = "will-supply" - SupplierWillSupplyCond MessageEvent = "will-supply-conditional" - SupplierLoaned MessageEvent = "loaned" - SupplierCompleted MessageEvent = "completed" - SupplierUnfilled MessageEvent = "unfilled" - SupplierCancelAccepted MessageEvent = "cancel-accepted" - SupplierCancelRejected MessageEvent = "cancel-rejected" - RequesterCancelRequest MessageEvent = "cancel-request" - RequesterReceived MessageEvent = "received" - RequesterShippedReturn MessageEvent = "shipped-return" - RequesterCondAccepted MessageEvent = "conditions-accepted" - RequesterCondRejected MessageEvent = "condition-rejected" + SupplierExpectToSupply MessageEvent = "expect-to-supply" + SupplierWillSupply MessageEvent = "will-supply" + SupplierWillSupplyCond MessageEvent = "will-supply-conditional" + SupplierLoaned MessageEvent = "loaned" + SupplierCompleted MessageEvent = "completed" + SupplierUnfilled MessageEvent = "unfilled" + SupplierCancelAccepted MessageEvent = "cancel-accepted" + SupplierCancelRejected MessageEvent = "cancel-rejected" + RequesterCancelRequest MessageEvent = "cancel-request" + RequesterReceived MessageEvent = "received" + RequesterShippedReturn MessageEvent = "shipped-return" + RequesterCondAccepted MessageEvent = "conditions-accepted" + RequesterCondRejected MessageEvent = "condition-rejected" + SupplierRetryConditional MessageEvent = "retry-conditional" ) func requesterBuiltInStates() []string { @@ -104,6 +112,9 @@ func requesterBuiltInStates() []string { string(BorrowerStateCompleted), string(BorrowerStateCancelled), string(BorrowerStateUnfilled), + string(BorrowerStateRetryAccepted), + string(BorrowerStateRetryRejected), + string(BorrowerStateRetryPending), }) } @@ -121,6 +132,7 @@ func supplierBuiltInStates() []string { string(LenderStateCompleted), string(LenderStateCancelled), string(LenderStateUnfilled), + string(LenderStateCompletedWithRetry), }) } @@ -162,6 +174,14 @@ func requesterBuiltInActions() []proapi.ActionCapability { Name: string(BorrowerActionShipReturn), Parameters: []string{}, }, + { + Name: string(BorrowerActionAcceptRetry), + Parameters: []string{}, + }, + { + Name: string(BorrowerActionRejectRetry), + Parameters: []string{}, + }, } } @@ -211,6 +231,10 @@ func supplierBuiltInActions() []proapi.ActionCapability { Name: string(LenderActionAcceptCancel), Parameters: []string{}, }, + { + Name: string(LenderActionAskRetry), + Parameters: []string{}, + }, } } @@ -234,6 +258,7 @@ func supplierBuiltInMessageEvents() []string { string(SupplierUnfilled), string(SupplierCancelAccepted), string(SupplierCancelRejected), + string(SupplierRetryConditional), }) } diff --git a/broker/patron_request/service/statemodels/returnables.json b/broker/patron_request/service/statemodels/returnables.json index 179df8d4..134e4d3a 100644 --- a/broker/patron_request/service/statemodels/returnables.json +++ b/broker/patron_request/service/statemodels/returnables.json @@ -69,9 +69,9 @@ "transition": "UNFILLED" }, { - "name": "accept-retry", - "desc": "Supplier accepts retry with new metadata", - "transition": "NEW" + "name": "retry-conditional", + "desc": "Received retry from supplier", + "transition": "RETRY_PENDING" } ] }, @@ -101,6 +101,11 @@ "desc": "Supplier will supply with conditions (ISO18626 WillSupply with conditions)", "transition": "CONDITION_PENDING" }, + { + "name": "retry-conditional", + "desc": "Received retry from supplier", + "transition": "RETRY_PENDING" + }, { "name": "loaned", "desc": "Supplier shipped the item (ISO18626 Loaned)", @@ -149,6 +154,30 @@ } ] }, + { + "name": "RETRY_PENDING", + "display": "Retry Pending", + "desc": "Received supplier retry request with new conditions after initial conditions were rejected", + "side": "REQUESTER", + "primaryAction": "accept-retry", + "needsAttention": true, + "actions": [ + { + "name": "accept-retry", + "desc": "Requester accepts supplier retry request", + "transitions": { + "success": "RETRY_ACCEPTED" + } + }, + { + "name": "reject-retry", + "desc": "Requester rejects supplier retry request", + "transitions": { + "success": "RETRY_REJECTED" + } + } + ] + }, { "name": "WILL_SUPPLY", "display": "Will Supply", @@ -298,6 +327,20 @@ "side": "REQUESTER", "terminal": true }, + { + "name": "RETRY_ACCEPTED", + "display": "Retry Accepted", + "desc": "Retry request is accepted by supplier with new metadata", + "side": "REQUESTER", + "terminal": true + }, + { + "name": "RETRY_REJECTED", + "display": "Retry Rejected", + "desc": "Retry request is rejected by supplier", + "side": "REQUESTER", + "terminal": true + }, { "name": "NEW", "display": "New", @@ -348,7 +391,7 @@ "name": "ask-retry", "desc": "Ask requester to retry with new ISO18626 metadata", "transitions": { - "success": "RETRY_REQUESTED" + "success": "COMPLETED_WITH_RETRY" } } ], @@ -561,9 +604,9 @@ "terminal": true }, { - "name": "RETRY_REQUESTED", - "display": "Retry Requested", - "desc": "After manual retry request", + "name": "COMPLETED_WITH_RETRY", + "display": "Completed with Retry", + "desc": "After sending retry to requester", "side": "SUPPLIER", "terminal": true } diff --git a/misc/returnables.yaml b/misc/returnables.yaml index 072ed1e9..94e1bb9a 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -48,9 +48,9 @@ states: - name: unfilled desc: Supplier cannot supply (ISO18626 Unfilled) transition: UNFILLED - - name: accept-retry - desc: Supplier accepts retry with new metadata - transition: NEW + - name: retry-conditional + desc: Received retry from supplier + transition: RETRY_PENDING - name: SUPPLIER_LOCATED display: Supplier Located @@ -69,6 +69,9 @@ states: - name: will-supply-conditional desc: Supplier will supply with conditions (ISO18626 WillSupply with conditions) transition: CONDITION_PENDING + - name: retry-conditional + desc: Received retry from supplier + transition: RETRY_PENDING - name: loaned desc: Supplier shipped the item (ISO18626 Loaned) transition: SHIPPED @@ -99,6 +102,22 @@ states: desc: Supplier cannot supply (ISO18626 Unfilled) transition: UNFILLED + - name: RETRY_PENDING + display: Retry Pending + desc: Received supplier retry request with new conditions after initial conditions were rejected + side: REQUESTER + primaryAction: accept-retry + needsAttention: true + actions: + - name: accept-retry + desc: Requester accepts supplier retry request + transitions: + success: RETRY_ACCEPTED + - name: reject-retry + desc: Requester rejects supplier retry request + transitions: + success: RETRY_REJECTED + - name: WILL_SUPPLY display: Will Supply desc: Received ISO18626 WillSupply (without condition) @@ -203,6 +222,18 @@ states: side: REQUESTER terminal: true + - name: RETRY_ACCEPTED + display: Retry Accepted + desc: Retry request is accepted by supplier with new metadata + side: REQUESTER + terminal: true + + - name: RETRY_REJECTED + display: Retry Rejected + desc: Retry request is rejected by supplier + side: REQUESTER + terminal: true + # Supplier-side - name: NEW display: New @@ -238,7 +269,7 @@ states: - name: ask-retry desc: Ask requester to retry with new ISO18626 metadata transitions: - success: RETRY_REQUESTED + success: COMPLETED_WITH_RETRY events: - name: cancel-request desc: Requester sent ISO18626 Cancel @@ -377,8 +408,8 @@ states: side: SUPPLIER terminal: true - - name: RETRY_REQUESTED - display: Retry Requested - desc: After manual retry request + - name: COMPLETED_WITH_RETRY + display: Completed with Retry + desc: After sending retry to requester side: SUPPLIER terminal: true From fa8eb8c84696f7cc028202121410017d40441b8b Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 1 Jun 2026 14:38:03 +0200 Subject: [PATCH 04/44] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- misc/returnables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/returnables.yaml b/misc/returnables.yaml index 94e1bb9a..04763974 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -224,7 +224,7 @@ states: - name: RETRY_ACCEPTED display: Retry Accepted - desc: Retry request is accepted by supplier with new metadata + desc: Requester accepted supplier retry request with new metadata side: REQUESTER terminal: true From 920a937d18e200ef82361b71571c0d77763b4c70 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 1 Jun 2026 14:38:22 +0200 Subject: [PATCH 05/44] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- misc/returnables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/returnables.yaml b/misc/returnables.yaml index 04763974..13dd159b 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -230,7 +230,7 @@ states: - name: RETRY_REJECTED display: Retry Rejected - desc: Retry request is rejected by supplier + desc: Requester rejected supplier retry request side: REQUESTER terminal: true From ddb2333caccac435f9d17fd211bcc4f50ec1d1cd Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 1 Jun 2026 14:40:12 +0200 Subject: [PATCH 06/44] update returnables.json --- broker/patron_request/service/statemodels/returnables.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/broker/patron_request/service/statemodels/returnables.json b/broker/patron_request/service/statemodels/returnables.json index 134e4d3a..5d447604 100644 --- a/broker/patron_request/service/statemodels/returnables.json +++ b/broker/patron_request/service/statemodels/returnables.json @@ -330,14 +330,14 @@ { "name": "RETRY_ACCEPTED", "display": "Retry Accepted", - "desc": "Retry request is accepted by supplier with new metadata", + "desc": "Requester accepted supplier retry request with new metadata", "side": "REQUESTER", "terminal": true }, { "name": "RETRY_REJECTED", "display": "Retry Rejected", - "desc": "Retry request is rejected by supplier", + "desc": "Requester rejected supplier retry request", "side": "REQUESTER", "terminal": true }, From 4f1c54561340a1e5d6802088fd57c0387be0f1e1 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 10:57:23 +0200 Subject: [PATCH 07/44] Implement ask-retry --- broker/patron_request/service/action.go | 30 ++++++++++ broker/patron_request/service/action_test.go | 60 ++++++++++++++++++++ iso18626/opencodes.go | 1 + 3 files changed, 91 insertions(+) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 9558ab2b..73ebdda4 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -51,6 +51,8 @@ type actionParams struct { Cost *float64 `json:"cost,omitempty"` Currency string `json:"currency,omitempty"` ReasonUnfilled string `json:"reasonUnfilled,omitempty"` + ReasonRetry string `json:"reasonRetry,omitempty"` + ItemId string `json:"itemId,omitempty"` } func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) *PatronRequestActionService { @@ -329,6 +331,8 @@ func (a *PatronRequestActionService) handleLenderAction(ctx common.ExtendedConte return a.markReceivedLenderRequest(ctx, pr, lms) case LenderActionAcceptCancel: return a.acceptCancelLenderRequest(ctx, pr) + case LenderActionAskRetry: + return a.askRetryLenderRequest(ctx, pr, params) default: status, result := logActionErrorAndReturnResult(ctx, "lender action "+string(action)+" is not implemented yet", errors.New("invalid action")) return actionExecutionResult{status: status, result: result, pr: pr} @@ -772,6 +776,32 @@ func (a *PatronRequestActionService) acceptCancelLenderRequest(ctx common.Extend return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } +func (a *PatronRequestActionService) askRetryLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, params actionParams) actionExecutionResult { + var deliveryInfo *iso18626.DeliveryInfo + if params.ItemId != "" { + deliveryInfo = &iso18626.DeliveryInfo{ + ItemId: params.ItemId, + } + } + reasonRetry := string(iso18626.ReasonRetryNotFoundAsCited) + if params.ReasonRetry != "" { + reasonRetry = params.ReasonRetry + } + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, + iso18626.MessageInfo{ + ReasonRetry: &iso18626.TypeSchemeValuePair{Text: reasonRetry}, + ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, + Note: params.Note, + }, + iso18626.StatusInfo{Status: iso18626.TypeStatusRetryPossible}, + deliveryInfo) + if result.OutgoingMessage.SupplyingAgencyMessage != nil { + setSupplierMessage(*result.OutgoingMessage.SupplyingAgencyMessage, &pr) + } + return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) +} + func (a *PatronRequestActionService) checkSupplyingResponse(status events.EventStatus, eventResult *events.EventResult, result *events.EventResult, httpStatus *int, pr pr_db.PatronRequest) actionExecutionResult { if httpStatus == nil { return actionExecutionResult{status: status, result: eventResult, pr: pr} diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index e553e90d..5fb08974 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -1097,6 +1097,66 @@ func TestHandleInvokeLenderActionAddConditionOK(t *testing.T) { } } +func TestHandleInvokeLenderActionAskRetryMinimal(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", "ISIL:SUP1").Return(lms.CreateLmsAdapterMockOK(), nil) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := iso18626.Request{} + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionAskRetry + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{}, + }}) + assert.Equal(t, events.EventStatusSuccess, status) + assert.NotNil(t, resultData) + assert.Equal(t, LenderStateCompletedWithRetry, mockPrRepo.savedPr.State) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusRetryPossible, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, string(iso18626.ReasonRetryNotFoundAsCited), mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonRetry.Text) + assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) + } +} + +func TestHandleInvokeLenderActionAskRetryFull(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", "ISIL:SUP1").Return(lms.CreateLmsAdapterMockOK(), nil) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := iso18626.Request{} + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionAskRetry + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{ + "note": "isbn", + "itemId": "0201896834", + "reasonRetry": "Transfer", + }, + }}) + assert.Equal(t, events.EventStatusSuccess, status) + assert.NotNil(t, resultData) + assert.Equal(t, LenderStateCompletedWithRetry, mockPrRepo.savedPr.State) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusRetryPossible, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "Transfer", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonRetry.Text) + assert.Equal(t, "isbn", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) { + assert.Equal(t, "0201896834", mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo.ItemId) + } + } +} + func TestHandleInvokeLenderActionAddConditionMissingConditionAndCost(t *testing.T) { mockPrRepo := new(MockPrRepo) lmsCreator := new(MockLmsCreator) diff --git a/iso18626/opencodes.go b/iso18626/opencodes.go index d32653ab..dacae20c 100644 --- a/iso18626/opencodes.go +++ b/iso18626/opencodes.go @@ -9,6 +9,7 @@ const ( ReasonRetryCostExceedsMaxCost ReasonRetry = "CostExceedsMaxCost" ReasonRetryOnLoan ReasonRetry = "OnLoan" ReasonRetryLoanCondition ReasonRetry = "LoanCondition" + ReasonRetryNotFoundAsCited ReasonRetry = "NotFoundAsCited" ) type ReasonUnfilled string From e7bf570c5de9223e2b67ddb4a9495f67123e213d Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 12:20:44 +0200 Subject: [PATCH 08/44] Create retry-conditonal event --- broker/patron_request/service/message-handler.go | 2 ++ broker/patron_request/service/statemodel_capabilities.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index 964a268c..7b37ccff 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -267,6 +267,8 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } eventName = SupplierCancelAccepted } + case iso18626.TypeStatusRetryPossible: + eventName = SupplierRetryConditional } if eventName == "" { diff --git a/broker/patron_request/service/statemodel_capabilities.go b/broker/patron_request/service/statemodel_capabilities.go index 62fc33d7..97cfa0ed 100644 --- a/broker/patron_request/service/statemodel_capabilities.go +++ b/broker/patron_request/service/statemodel_capabilities.go @@ -87,12 +87,12 @@ const ( SupplierUnfilled MessageEvent = "unfilled" SupplierCancelAccepted MessageEvent = "cancel-accepted" SupplierCancelRejected MessageEvent = "cancel-rejected" + SupplierRetryConditional MessageEvent = "retry-conditional" RequesterCancelRequest MessageEvent = "cancel-request" RequesterReceived MessageEvent = "received" RequesterShippedReturn MessageEvent = "shipped-return" RequesterCondAccepted MessageEvent = "conditions-accepted" RequesterCondRejected MessageEvent = "condition-rejected" - SupplierRetryConditional MessageEvent = "retry-conditional" ) func requesterBuiltInStates() []string { From 524a26a7ff914ca119f33ddf81799ec542555906 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 12:21:50 +0200 Subject: [PATCH 09/44] illmock: RETRY:NOTFOUNDASCITED --- illmock/README.md | 1 + illmock/app/app_test.go | 17 +++++++++++++++++ illmock/app/supplier.go | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/illmock/README.md b/illmock/README.md index 462ebb0f..0f4ef949 100644 --- a/illmock/README.md +++ b/illmock/README.md @@ -82,6 +82,7 @@ The scenario is used by the supplier to perform a particular workflow. The follo |`RETRY:COND_` ... | Response with `RetryPossible` and ReasonRetry `LoanCondition` | |`RETRY:COST_` ... | Response with `RetryPossible` and ReasonRetry+ReasonUnfilled `CostExceedsMaxCost` | |`RETRY:ONLOAN_` ... | Response with `RetryPossible` and ReasonRetry `OnLoan` | +|`RETRY:NOTFOUNDASCITED` | Response with `RetryPossible` and ReasonRetry `NotFoundAsCited` | ### Delivery method diff --git a/illmock/app/app_test.go b/illmock/app/app_test.go index b96b5c82..027cfc15 100644 --- a/illmock/app/app_test.go +++ b/illmock/app/app_test.go @@ -1370,6 +1370,23 @@ func TestService(t *testing.T) { assert.Equal(t, iso18626.TypeActionShippedReturn, *responseMsg.RequestingAgencyMessageConfirmation.Action) }) + t.Run("Patron request not found as cited", func(t *testing.T) { + msg := createPatronRequest() + ret := runScenario(t, isoUrl, apiUrl, msg, "RETRY:NOTFOUNDASCITED", 8) + + m := ret[1].Message + rid := m.Request.Header.RequestingAgencyRequestId + + m = ret[len(ret)-2].Message + assert.NotNil(t, m.SupplyingAgencyMessage) + assert.Equal(t, iso18626.TypeStatusRetryPossible, m.SupplyingAgencyMessage.StatusInfo.Status) + + m = ret[len(ret)-1].Message + assert.NotNil(t, m.SupplyingAgencyMessageConfirmation) + assert.Equal(t, rid, m.SupplyingAgencyMessageConfirmation.ConfirmationHeader.RequestingAgencyRequestId) + assert.Equal(t, iso18626.TypeReasonForMessageRequestResponse, *m.SupplyingAgencyMessageConfirmation.ReasonForMessage) + }) + t.Run("tenant ID set", func(t *testing.T) { assert.Equal(t, "T1", app.client.Headers.Get("X-Okapi-Tenant")) }) diff --git a/illmock/app/supplier.go b/illmock/app/supplier.go index d0c4efba..52383572 100644 --- a/illmock/app/supplier.go +++ b/illmock/app/supplier.go @@ -138,6 +138,10 @@ func (app *MockApp) handleSupplierRequest(illRequest *iso18626.Request, w http.R status = append(status, iso18626.TypeStatusRetryPossible) x := iso18626.ReasonRetryLoanCondition reasonRetry = &x + case "RETRY:NOTFOUNDASCITED": + status = append(status, iso18626.TypeStatusRetryPossible) + x := iso18626.ReasonRetryNotFoundAsCited + reasonRetry = &x case "COMPLETED": if illRequest.ServiceInfo != nil && illRequest.ServiceInfo.ServiceType == iso18626.TypeServiceTypeCopy { status = append(status, iso18626.TypeStatusCopyCompleted) @@ -289,6 +293,8 @@ func (app *MockApp) sendSupplyingAgencyLater(header *iso18626.Header, statusList case iso18626.ReasonRetryLoanCondition: msg.SupplyingAgencyMessage.DeliveryInfo = &iso18626.DeliveryInfo{} msg.SupplyingAgencyMessage.DeliveryInfo.LoanCondition = &iso18626.TypeSchemeValuePair{Text: "NoReproduction"} + case iso18626.ReasonRetryNotFoundAsCited: + // no special handling for this one, just a reason for retry } } if state.presentResponse { From 95290723fe7812717282d04fec258090563490f0 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 13:37:19 +0200 Subject: [PATCH 10/44] reject-retry --- broker/patron_request/service/action.go | 7 + .../patron_request/api/api-handler_test.go | 137 ++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 73ebdda4..d2b14e54 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -277,6 +277,8 @@ func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedCo return a.acceptConditionBorrowingRequest(ctx, pr) case BorrowerActionRejectCondition: return a.rejectConditionBorrowingRequest(ctx, pr) + case BorrowerActionRejectRetry: + return a.rejectRetryBorrowingRequest(pr) default: status, result := logActionErrorAndReturnResult(ctx, "borrower action "+string(action)+" is not implemented yet", errors.New("invalid action")) return actionExecutionResult{status: status, result: result, pr: pr} @@ -532,6 +534,11 @@ func (a *PatronRequestActionService) rejectConditionBorrowingRequest(ctx common. return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } +func (a *PatronRequestActionService) rejectRetryBorrowingRequest(pr pr_db.PatronRequest) actionExecutionResult { + result := events.EventResult{} + return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} +} + func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lms lms.LmsAdapter) actionExecutionResult { institutionalPatron := lms.InstitutionalPatron(pr.RequesterSymbol.String) _, err := lms.LookupUser(institutionalPatron) diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 21b9aa71..beeac982 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -638,6 +638,143 @@ func TestActionsToCompleteState(t *testing.T) { assert.Equal(t, int64(len(events.Items)), events.About.Count) } +func TestRetryNotFoundAsCited(t *testing.T) { + requesterSymbol := "localISIL:REQ" + uuid.NewString() + supplierSymbol := "ISIL:SUP" + uuid.NewString() + + lmsConfig := &directory.LmsConfig{ + FromAgency: "from-agency", + Address: ncipMockUrl, + } + reqPeer := apptest.CreatePeerWithModeAndVendor(t, illRepo, requesterSymbol, adapter.MOCK_PEER_URL, app.BROKER_MODE, directory.CrossLink, + directory.Entry{ + LmsConfig: lmsConfig, + }) + assert.NotNil(t, reqPeer) + supPeer := apptest.CreatePeer(t, illRepo, supplierSymbol, adapter.MOCK_PEER_URL) + assert.NotNil(t, supPeer) + + // POST + patron := "p1" + request := iso18626.Request{ + BibliographicInfo: iso18626.BibliographicInfo{ + SupplierUniqueRecordId: "RETRY:NOTFOUNDASCITED", + }, + ServiceInfo: &iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Copy", + }, + ServiceType: iso18626.TypeServiceTypeCopy, + NeedBeforeDate: &utils.XSDDateTime{ + Time: time.Now().Add(24 * time.Hour), + }, + }, + PatronInfo: &iso18626.PatronInfo{ + GivenName: "John", + Surname: "Wick", + }, + } + id := "REQ-" + strings.ToUpper(uuid.NewString()) + newPr := proapi.CreatePatronRequest{ + Id: &id, + RequesterSymbol: &requesterSymbol, + Patron: &patron, + IllRequest: request, + } + newPrBytes, err := json.Marshal(newPr) + assert.NoError(t, err, "failed to marshal patron request") + + hres, respBytes := httpRequest2(t, "POST", basePath, newPrBytes, 201) + // Check Location header + location := hres.Header.Get("Location") + assert.NotEmpty(t, location, "Location header should be set") + assert.Equal(t, getLocalhostWithPort()+"/patron_requests/"+id, location) + + var foundPr proapi.PatronRequest + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + + assert.Equal(t, *newPr.Id, foundPr.Id) + assert.True(t, foundPr.State != "") + assert.Equal(t, string(prservice.SideBorrowing), foundPr.Side) + assert.Equal(t, *newPr.RequesterSymbol, *foundPr.RequesterSymbol) + assert.Nil(t, foundPr.SupplierSymbol) + assert.Equal(t, *newPr.Patron, *foundPr.Patron) + assertPatronRequestIllRequest(t, foundPr.IllRequest, func(r iso18626.Request) { + assert.Equal(t, "RETRY:NOTFOUNDASCITED", r.BibliographicInfo.SupplierUniqueRecordId) + assert.Equal(t, *newPr.Id, r.Header.RequestingAgencyRequestId) + assert.False(t, r.Header.Timestamp.IsZero()) + }) + assert.Equal(t, "validate", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.NotNil(t, foundPr.NotificationsLink) + + // GET list + queryParams := "?side=borrowing&symbol=" + *foundPr.RequesterSymbol + respBytes = httpRequest(t, "GET", basePath+queryParams, []byte{}, 200) + var foundPrs proapi.PatronRequests + err = json.Unmarshal(respBytes, &foundPrs) + assert.NoError(t, err, "failed to unmarshal patron request") + + thisPrPath := basePath + "/" + *newPr.Id + + // POST execute action + action := proapi.ExecuteAction{ + Action: "send-request", + } + actionBytes, err := json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 200) + var pResult proapi.ActionResult + err = json.Unmarshal(respBytes, &pResult) + assert.NoError(t, err, "failed to unmarshal patron request action result") + assert.Equal(t, "SUCCESS", pResult.Result) + assert.Equal(t, "success", pResult.Outcome) + assert.Equal(t, "VALIDATED", pResult.FromState) + assert.Equal(t, "SENT", *pResult.ToState) + assert.Nil(t, pResult.Message) + + respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, *newPr.Id, foundPr.Id) + assert.Equal(t, "send-request", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + + // Wait until we can see possible action reject-retry + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) + return strings.Contains(string(respBytes), "\"name\":\"reject-retry\"") + }) + respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) + assert.Contains(t, string(respBytes), "\"name\":\"reject-retry\"") + + // POST blocking action + action = proapi.ExecuteAction{ + Action: "reject-retry", + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 200) + err = json.Unmarshal(respBytes, &pResult) + assert.NoError(t, err, "failed to unmarshal patron request action result") + assert.Equal(t, "SUCCESS", pResult.Result) + + respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, *newPr.Id, foundPr.Id) + assert.Equal(t, "reject-retry", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + + // reject again - should fail as the request state it terminated + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 400) + assert.Contains(t, string(respBytes), "Action reject-retry is not allowed for patron request") +} + func TestPostPatronRequestRejectsInvalidIllRequest(t *testing.T) { requesterSymbol := "localISIL:REQ" + uuid.NewString() From 0b7be58a18c5d747544fce4e230fee757103c67a Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 14:29:26 +0200 Subject: [PATCH 11/44] illmock: sets DeliveryInfo.ItemId on RETRY:NOTFOUNDASCITED --- illmock/app/app_test.go | 2 ++ illmock/app/supplier.go | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/illmock/app/app_test.go b/illmock/app/app_test.go index 027cfc15..59edb511 100644 --- a/illmock/app/app_test.go +++ b/illmock/app/app_test.go @@ -1380,6 +1380,8 @@ func TestService(t *testing.T) { m = ret[len(ret)-2].Message assert.NotNil(t, m.SupplyingAgencyMessage) assert.Equal(t, iso18626.TypeStatusRetryPossible, m.SupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, string(iso18626.ReasonRetryNotFoundAsCited), m.SupplyingAgencyMessage.MessageInfo.ReasonRetry.Text) + assert.Equal(t, "123456789", m.SupplyingAgencyMessage.DeliveryInfo.ItemId) m = ret[len(ret)-1].Message assert.NotNil(t, m.SupplyingAgencyMessageConfirmation) diff --git a/illmock/app/supplier.go b/illmock/app/supplier.go index 52383572..e8ad6101 100644 --- a/illmock/app/supplier.go +++ b/illmock/app/supplier.go @@ -294,7 +294,9 @@ func (app *MockApp) sendSupplyingAgencyLater(header *iso18626.Header, statusList msg.SupplyingAgencyMessage.DeliveryInfo = &iso18626.DeliveryInfo{} msg.SupplyingAgencyMessage.DeliveryInfo.LoanCondition = &iso18626.TypeSchemeValuePair{Text: "NoReproduction"} case iso18626.ReasonRetryNotFoundAsCited: - // no special handling for this one, just a reason for retry + msg.SupplyingAgencyMessage.DeliveryInfo = &iso18626.DeliveryInfo{ + ItemId: "123456789", + } } } if state.presentResponse { From d1f991232f2693266ebbeee2dee6b5a925645488 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 15:02:30 +0200 Subject: [PATCH 12/44] acceptRetry begin --- broker/patron_request/service/action.go | 8 + .../patron_request/service/message-handler.go | 9 ++ .../patron_request/api/api-handler_test.go | 147 +++++++++++++++++- 3 files changed, 156 insertions(+), 8 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index d2b14e54..6cdbe4f2 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -279,6 +279,8 @@ func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedCo return a.rejectConditionBorrowingRequest(ctx, pr) case BorrowerActionRejectRetry: return a.rejectRetryBorrowingRequest(pr) + case BorrowerActionAcceptRetry: + return a.acceptRetryBorrowingRequest(pr) default: status, result := logActionErrorAndReturnResult(ctx, "borrower action "+string(action)+" is not implemented yet", errors.New("invalid action")) return actionExecutionResult{status: status, result: result, pr: pr} @@ -539,6 +541,12 @@ func (a *PatronRequestActionService) rejectRetryBorrowingRequest(pr pr_db.Patron return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } +func (a *PatronRequestActionService) acceptRetryBorrowingRequest(pr pr_db.PatronRequest) actionExecutionResult { + // TODO: link request IDs + result := events.EventResult{} + return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} +} + func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lms lms.LmsAdapter) actionExecutionResult { institutionalPatron := lms.InstitutionalPatron(pr.RequesterSymbol.String) _, err := lms.LookupUser(institutionalPatron) diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index 7b37ccff..19c103ab 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -229,6 +229,7 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } eventName := MessageEvent("") + retryItemId := "" switch sam.StatusInfo.Status { case iso18626.TypeStatusExpectToSupply: eventName = SupplierExpectToSupply @@ -269,6 +270,9 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } case iso18626.TypeStatusRetryPossible: eventName = SupplierRetryConditional + if sam.DeliveryInfo != nil { + retryItemId = sam.DeliveryInfo.ItemId + } } if eventName == "" { @@ -285,6 +289,11 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx if !eventDefined { return statusChangeNotAllowed() } + if retryItemId != "" { + // needs to be stored in the patron request so it can be used if the requester accepts the retry offer + // could be a field on its own (extend the db schema) + updatedPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId = retryItemId + } return m.updatePatronRequestAndCreateSamResponse(ctx, updatedPr, sam, stateChanged, parentEventID) } diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index beeac982..2bc3c4c8 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -638,7 +638,7 @@ func TestActionsToCompleteState(t *testing.T) { assert.Equal(t, int64(len(events.Items)), events.About.Count) } -func TestRetryNotFoundAsCited(t *testing.T) { +func TestRejectRetry(t *testing.T) { requesterSymbol := "localISIL:REQ" + uuid.NewString() supplierSymbol := "ISIL:SUP" + uuid.NewString() @@ -665,13 +665,6 @@ func TestRetryNotFoundAsCited(t *testing.T) { Text: "Copy", }, ServiceType: iso18626.TypeServiceTypeCopy, - NeedBeforeDate: &utils.XSDDateTime{ - Time: time.Now().Add(24 * time.Hour), - }, - }, - PatronInfo: &iso18626.PatronInfo{ - GivenName: "John", - Surname: "Wick", }, } id := "REQ-" + strings.ToUpper(uuid.NewString()) @@ -775,6 +768,144 @@ func TestRetryNotFoundAsCited(t *testing.T) { assert.Contains(t, string(respBytes), "Action reject-retry is not allowed for patron request") } +func TestAcceptRetry(t *testing.T) { + requesterSymbol := "localISIL:REQ" + uuid.NewString() + supplierSymbol := "ISIL:SUP" + uuid.NewString() + + lmsConfig := &directory.LmsConfig{ + FromAgency: "from-agency", + Address: ncipMockUrl, + } + reqPeer := apptest.CreatePeerWithModeAndVendor(t, illRepo, requesterSymbol, adapter.MOCK_PEER_URL, app.BROKER_MODE, directory.CrossLink, + directory.Entry{ + LmsConfig: lmsConfig, + }) + assert.NotNil(t, reqPeer) + supPeer := apptest.CreatePeer(t, illRepo, supplierSymbol, adapter.MOCK_PEER_URL) + assert.NotNil(t, supPeer) + + // POST + patron := "p1" + request := iso18626.Request{ + BibliographicInfo: iso18626.BibliographicInfo{ + SupplierUniqueRecordId: "RETRY:NOTFOUNDASCITED", + BibliographicItemId: []iso18626.BibliographicItemId{ + { + BibliographicItemIdentifierCode: iso18626.TypeSchemeValuePair{ + Text: "ISBN", + }, + BibliographicItemIdentifier: "1234567890", + }, + }, + }, + ServiceInfo: &iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Copy", + }, + ServiceType: iso18626.TypeServiceTypeCopy, + }, + } + id := "REQ-" + strings.ToUpper(uuid.NewString()) + newPr := proapi.CreatePatronRequest{ + Id: &id, + RequesterSymbol: &requesterSymbol, + Patron: &patron, + IllRequest: request, + } + newPrBytes, err := json.Marshal(newPr) + assert.NoError(t, err, "failed to marshal patron request") + + hres, respBytes := httpRequest2(t, "POST", basePath, newPrBytes, 201) + // Check Location header + location := hres.Header.Get("Location") + assert.NotEmpty(t, location, "Location header should be set") + assert.Equal(t, getLocalhostWithPort()+"/patron_requests/"+id, location) + + var foundPr proapi.PatronRequest + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + + assert.Equal(t, *newPr.Id, foundPr.Id) + assert.True(t, foundPr.State != "") + assert.Equal(t, string(prservice.SideBorrowing), foundPr.Side) + assert.Equal(t, *newPr.RequesterSymbol, *foundPr.RequesterSymbol) + assert.Nil(t, foundPr.SupplierSymbol) + assert.Equal(t, *newPr.Patron, *foundPr.Patron) + assertPatronRequestIllRequest(t, foundPr.IllRequest, func(r iso18626.Request) { + assert.Equal(t, "RETRY:NOTFOUNDASCITED", r.BibliographicInfo.SupplierUniqueRecordId) + assert.Equal(t, *newPr.Id, r.Header.RequestingAgencyRequestId) + assert.False(t, r.Header.Timestamp.IsZero()) + }) + assert.Equal(t, "validate", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.NotNil(t, foundPr.NotificationsLink) + + // GET list + queryParams := "?side=borrowing&symbol=" + *foundPr.RequesterSymbol + respBytes = httpRequest(t, "GET", basePath+queryParams, []byte{}, 200) + var foundPrs proapi.PatronRequests + err = json.Unmarshal(respBytes, &foundPrs) + assert.NoError(t, err, "failed to unmarshal patron request") + + thisPrPath := basePath + "/" + *newPr.Id + + // POST execute action + action := proapi.ExecuteAction{ + Action: "send-request", + } + actionBytes, err := json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 200) + var pResult proapi.ActionResult + err = json.Unmarshal(respBytes, &pResult) + assert.NoError(t, err, "failed to unmarshal patron request action result") + assert.Equal(t, "SUCCESS", pResult.Result) + assert.Equal(t, "success", pResult.Outcome) + assert.Equal(t, "VALIDATED", pResult.FromState) + assert.Equal(t, "SENT", *pResult.ToState) + assert.Nil(t, pResult.Message) + + respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, *newPr.Id, foundPr.Id) + assert.Equal(t, "send-request", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + + // Wait until we can see possible action accept-retry + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) + return strings.Contains(string(respBytes), "\"name\":\"accept-retry\"") + }) + respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) + assert.Contains(t, string(respBytes), "\"name\":\"accept-retry\"") + + // POST blocking action + action = proapi.ExecuteAction{ + Action: "accept-retry", + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 200) + err = json.Unmarshal(respBytes, &pResult) + assert.NoError(t, err, "failed to unmarshal patron request action result") + assert.Equal(t, "SUCCESS", pResult.Result) + + respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, *newPr.Id, foundPr.Id) + assert.Equal(t, "accept-retry", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + + // accept again - should fail as the request state it terminated + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 400) + assert.Contains(t, string(respBytes), "Action accept-retry is not allowed for patron request") +} + func TestPostPatronRequestRejectsInvalidIllRequest(t *testing.T) { requesterSymbol := "localISIL:REQ" + uuid.NewString() From 95586a0d2c9f35723638acc772fd6094f36a53fe Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 2 Jun 2026 17:09:09 +0200 Subject: [PATCH 13/44] Add {next,prev}_req_id --- .../migrations/042_add_patron_links.down.sql | 27 +++++++++++++++++ broker/migrations/042_add_patron_links.up.sql | 30 +++++++++++++++++++ broker/oapi/open-api.yaml | 6 ++++ broker/patron_request/api/api-handler.go | 2 ++ broker/patron_request/db/prcql.go | 2 ++ broker/sqlc/pr_query.sql | 10 ++++--- broker/sqlc/pr_schema.sql | 4 ++- 7 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 broker/migrations/042_add_patron_links.down.sql create mode 100644 broker/migrations/042_add_patron_links.up.sql diff --git a/broker/migrations/042_add_patron_links.down.sql b/broker/migrations/042_add_patron_links.down.sql new file mode 100644 index 00000000..6453b513 --- /dev/null +++ b/broker/migrations/042_add_patron_links.down.sql @@ -0,0 +1,27 @@ +ALTER TABLE patron_request DROP COLUMN next_req_id; +ALTER TABLE patron_request DROP COLUMN prev_req_id; + +CREATE VIEW patron_request_search_view AS +SELECT + pr.*, + EXISTS ( + SELECT 1 + FROM notification n + WHERE n.pr_id = pr.id + ) AS has_notification, + EXISTS ( + SELECT 1 + FROM notification n + WHERE n.pr_id = pr.id and cost is not null + ) AS has_cost, + (unread.unread_notifications_count > 0) AS has_unread_notification, + pr.ill_request -> 'serviceInfo' ->> 'serviceType' AS service_type, + pr.ill_request -> 'serviceInfo' -> 'serviceLevel' ->> '#text' AS service_level, + immutable_to_timestamp(pr.ill_request -> 'serviceInfo' ->> 'needBeforeDate') AS needed_at, + unread.unread_notifications_count AS unread_notifications_count +FROM patron_request pr +LEFT JOIN LATERAL ( + SELECT COUNT(*) AS unread_notifications_count + FROM notification n + WHERE n.pr_id = pr.id and n.acknowledged_at is null +) unread ON true; diff --git a/broker/migrations/042_add_patron_links.up.sql b/broker/migrations/042_add_patron_links.up.sql new file mode 100644 index 00000000..f9f31fe0 --- /dev/null +++ b/broker/migrations/042_add_patron_links.up.sql @@ -0,0 +1,30 @@ +ALTER TABLE patron_request ADD COLUMN next_req_id VARCHAR; +ALTER TABLE patron_request ADD COLUMN prev_req_id VARCHAR; + +DROP VIEW IF EXISTS patron_request_search_view; + +CREATE VIEW patron_request_search_view AS +SELECT + pr.*, + EXISTS ( + SELECT 1 + FROM notification n + WHERE n.pr_id = pr.id + ) AS has_notification, + EXISTS ( + SELECT 1 + FROM notification n + WHERE n.pr_id = pr.id and cost is not null + ) AS has_cost, + (unread.unread_notifications_count > 0) AS has_unread_notification, + (pr.internal_note IS NOT NULL AND btrim(pr.internal_note) <> '') AS has_internal_note, + pr.ill_request -> 'serviceInfo' ->> 'serviceType' AS service_type, + pr.ill_request -> 'serviceInfo' -> 'serviceLevel' ->> '#text' AS service_level, + immutable_to_timestamp(pr.ill_request -> 'serviceInfo' ->> 'needBeforeDate') AS needed_at, + unread.unread_notifications_count AS unread_notifications_count +FROM patron_request pr +LEFT JOIN LATERAL ( + SELECT COUNT(*) AS unread_notifications_count + FROM notification n + WHERE n.pr_id = pr.id and n.acknowledged_at is null +) unread ON true; diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index 4fa47b36..a6894a89 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -582,6 +582,12 @@ components: internalNote: type: string description: Staff-only internal note, local to this request and never shared with peers + nextReqId: + type: string + description: ID of the next patron request in the sequence + prevReqId: + type: string + description: ID of the previous patron request in the sequence required: - id - createdAt diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index 3ed0d7d2..cf7ca13f 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -914,6 +914,8 @@ func toApiPatronRequest(r *http.Request, request pr_db.PatronRequestSearchView) EventsLink: eventsLink, TerminalState: request.TerminalState, InternalNote: toString(request.InternalNote), + NextReqId: toString(request.NextReqID), + PrevReqId: toString(request.PrevReqID), } if request.UpdatedAt.Valid { pr.UpdatedAt = &request.UpdatedAt.Time diff --git a/broker/patron_request/db/prcql.go b/broker/patron_request/db/prcql.go index bb4dabb5..deb9fd57 100644 --- a/broker/patron_request/db/prcql.go +++ b/broker/patron_request/db/prcql.go @@ -293,6 +293,8 @@ func (q *Queries) ListPatronRequestsCql(ctx context.Context, db DBTX, arg ListPa &i.PatronRequestSearchView.UpdatedAt, &i.PatronRequestSearchView.IllResponse, &i.PatronRequestSearchView.InternalNote, + &i.PatronRequestSearchView.NextReqID, + &i.PatronRequestSearchView.PrevReqID, &i.PatronRequestSearchView.HasNotification, &i.PatronRequestSearchView.HasCost, &i.PatronRequestSearchView.HasUnreadNotification, diff --git a/broker/sqlc/pr_query.sql b/broker/sqlc/pr_query.sql index c0483223..76d749f3 100644 --- a/broker/sqlc/pr_query.sql +++ b/broker/sqlc/pr_query.sql @@ -55,14 +55,16 @@ SET ill_request = $3, language = $16, terminal_state = $17, updated_at = now(), - ill_response = $19, - internal_note = $20 + ill_response = $19, + internal_note = $20, + next_req_id = $21, + prev_req_id = $22 WHERE id = $1 AND created_at = $2 AND (updated_at is null OR updated_at = $18) RETURNING sqlc.embed(patron_request); -- name: CreatePatronRequest :one -INSERT INTO patron_request (id, created_at, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, items, language, terminal_state, updated_at, ill_response, internal_note) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) +INSERT INTO patron_request (id, created_at, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, items, language, terminal_state, updated_at, ill_response, internal_note, next_req_id, prev_req_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) RETURNING sqlc.embed(patron_request); -- name: UpdatePatronRequestInternalNote :exec diff --git a/broker/sqlc/pr_schema.sql b/broker/sqlc/pr_schema.sql index 3ecec8a0..6ae433d6 100644 --- a/broker/sqlc/pr_schema.sql +++ b/broker/sqlc/pr_schema.sql @@ -20,7 +20,9 @@ CREATE TABLE patron_request terminal_state BOOLEAN NOT NULL DEFAULT false, updated_at TIMESTAMP, ill_response jsonb NOT NULL DEFAULT '{}'::jsonb, - internal_note TEXT + internal_note TEXT, + next_req_id VARCHAR, + prev_req_id VARCHAR ); CREATE OR REPLACE FUNCTION get_next_hrid(prefix VARCHAR) RETURNS VARCHAR AS $$ From d6da9bb04738bea106bc7a16443d441963cfc102 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 08:00:26 +0200 Subject: [PATCH 14/44] Clone patronrequest WIP --- ...down.sql => 043_add_patron_links.down.sql} | 0 ...nks.up.sql => 043_add_patron_links.up.sql} | 0 broker/patron_request/service/action.go | 35 +++++++++++++++++-- .../patron_request/service/message-handler.go | 1 + .../patron_request/service/message_sender.go | 9 +++++ .../patron_request/api/api-handler_test.go | 26 ++++++++++++++ go.work.sum | 7 +++- 7 files changed, 74 insertions(+), 4 deletions(-) rename broker/migrations/{042_add_patron_links.down.sql => 043_add_patron_links.down.sql} (100%) rename broker/migrations/{042_add_patron_links.up.sql => 043_add_patron_links.up.sql} (100%) diff --git a/broker/migrations/042_add_patron_links.down.sql b/broker/migrations/043_add_patron_links.down.sql similarity index 100% rename from broker/migrations/042_add_patron_links.down.sql rename to broker/migrations/043_add_patron_links.down.sql diff --git a/broker/migrations/042_add_patron_links.up.sql b/broker/migrations/043_add_patron_links.up.sql similarity index 100% rename from broker/migrations/042_add_patron_links.up.sql rename to broker/migrations/043_add_patron_links.up.sql diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 6cdbe4f2..c2d526de 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -280,7 +280,7 @@ func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedCo case BorrowerActionRejectRetry: return a.rejectRetryBorrowingRequest(pr) case BorrowerActionAcceptRetry: - return a.acceptRetryBorrowingRequest(pr) + return a.acceptRetryBorrowingRequest(ctx, pr) default: status, result := logActionErrorAndReturnResult(ctx, "borrower action "+string(action)+" is not implemented yet", errors.New("invalid action")) return actionExecutionResult{status: status, result: result, pr: pr} @@ -541,9 +541,38 @@ func (a *PatronRequestActionService) rejectRetryBorrowingRequest(pr pr_db.Patron return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } -func (a *PatronRequestActionService) acceptRetryBorrowingRequest(pr pr_db.PatronRequest) actionExecutionResult { - // TODO: link request IDs +func clonePatronRequest(pr pr_db.PatronRequest) (pr_db.PatronRequest, error) { + prJSON, err := json.Marshal(pr) + if err != nil { + return pr_db.PatronRequest{}, err + } + var clone pr_db.PatronRequest + if err = json.Unmarshal(prJSON, &clone); err != nil { + return pr_db.PatronRequest{}, err + } + return clone, nil +} + +func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) actionExecutionResult { result := events.EventResult{} + + clone, err := clonePatronRequest(pr) + if err != nil { + status, result := logActionErrorAndReturnResult(ctx, "failed to clone patron request for retry", err) + return actionExecutionResult{status: status, result: result, pr: pr} + } + ctx.Logger().Info("cloned patron request for retry", "IllRequest.BibliographicInfo.SupplierUniqueRecordId", clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId) + clone.State = pr_db.PatronRequestState("VALIDATED") + clone.ID = uuid.NewString() + clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} + clone.PrevReqID = getDbTextPtr(&pr.ID) + pr.NextReqID = getDbTextPtr(&clone.ID) + + _, err = a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(clone)) + if err != nil { + status, result := logActionErrorAndReturnResult(ctx, "failed to create patron request for retry", err) + return actionExecutionResult{status: status, result: result, pr: pr} + } return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index 19c103ab..f4026e18 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -290,6 +290,7 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx return statusChangeNotAllowed() } if retryItemId != "" { + ctx.Logger().Info("AD: received retry possible status with retry item id", "retryItemId", retryItemId) // needs to be stored in the patron request so it can be used if the requester accepts the retry offer // could be a field on its own (extend the db schema) updatedPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId = retryItemId diff --git a/broker/patron_request/service/message_sender.go b/broker/patron_request/service/message_sender.go index 6611c8ae..0d5ed58b 100644 --- a/broker/patron_request/service/message_sender.go +++ b/broker/patron_request/service/message_sender.go @@ -142,6 +142,15 @@ func (ms *PatronRequestMessageSender) sendBorrowingRequest(ctx common.ExtendedCo illRequest.PatronInfo = &iso18626.PatronInfo{} } illRequest.PatronInfo.PatronId = pr.Patron.String + if illRequest.ServiceInfo == nil { + illRequest.ServiceInfo = &iso18626.ServiceInfo{} + } + requestType := iso18626.TypeRequestTypeNew + if pr.PrevReqID.Valid { + illRequest.ServiceInfo.RequestingAgencyPreviousRequestId = pr.PrevReqID.String + requestType = iso18626.TypeRequestTypeRetry + } + illRequest.ServiceInfo.RequestType = &requestType var illMessage = iso18626.NewISO18626Message() illMessage.Request = &illRequest diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 2bc3c4c8..d0467595 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -900,10 +900,36 @@ func TestAcceptRetry(t *testing.T) { assert.Equal(t, "accept-retry", *foundPr.LastAction) assert.Equal(t, "success", *foundPr.LastActionOutcome) assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.NotNil(t, foundPr.NextReqId, "got pr "+string(respBytes)) // accept again - should fail as the request state it terminated respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 400) assert.Contains(t, string(respBytes), "Action accept-retry is not allowed for patron request") + + // send request for the new PR created by accept-retry + thisPrPath = basePath + "/" + *foundPr.NextReqId + action = proapi.ExecuteAction{ + Action: "send-request", + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 200) + err = json.Unmarshal(respBytes, &pResult) + assert.NoError(t, err, "failed to unmarshal patron request action result") + assert.Equal(t, "SUCCESS", pResult.Result) + assert.Equal(t, "success", pResult.Outcome) + assert.Equal(t, "VALIDATED", pResult.FromState) + assert.Equal(t, "SENT", *pResult.ToState) + assert.Nil(t, pResult.Message) + + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) + return strings.Contains(string(respBytes), "\"name\":\"accept-retry\"") + }) + respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) + // TODO + // assert.Contains(t, string(respBytes), "\"name\":\"accept-retry\"") + } func TestPostPatronRequestRejectsInvalidIllRequest(t *testing.T) { diff --git a/go.work.sum b/go.work.sum index b2c6ca06..d1a14d07 100644 --- a/go.work.sum +++ b/go.work.sum @@ -770,6 +770,7 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -854,6 +855,7 @@ github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= @@ -917,6 +919,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= @@ -970,6 +973,7 @@ github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170/go.mod github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= @@ -1006,7 +1010,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= @@ -1079,6 +1082,7 @@ github.com/tonistiigi/go-archvariant v1.0.0/go.mod h1:TxFmO5VS6vMq2kvs3ht04iPXtu github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -1100,6 +1104,7 @@ github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= From 217bfb7a02329edbfd82f339bb07202aa35665e4 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 08:06:07 +0200 Subject: [PATCH 15/44] lint --- broker/test/patron_request/api/api-handler_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index d0467595..59a241da 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -929,7 +929,6 @@ func TestAcceptRetry(t *testing.T) { respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) // TODO // assert.Contains(t, string(respBytes), "\"name\":\"accept-retry\"") - } func TestPostPatronRequestRejectsInvalidIllRequest(t *testing.T) { From 7544ce88d210a3c891923b2a6abc50bf547b8551 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 13:24:07 +0200 Subject: [PATCH 16/44] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- broker/patron_request/service/statemodel_capabilities.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/broker/patron_request/service/statemodel_capabilities.go b/broker/patron_request/service/statemodel_capabilities.go index 97cfa0ed..fde30f74 100644 --- a/broker/patron_request/service/statemodel_capabilities.go +++ b/broker/patron_request/service/statemodel_capabilities.go @@ -232,8 +232,12 @@ func supplierBuiltInActions() []proapi.ActionCapability { Parameters: []string{}, }, { - Name: string(LenderActionAskRetry), - Parameters: []string{}, + Name: string(LenderActionAskRetry), + Parameters: []string{ + "note", + "reasonRetry", + "itemId", + }, }, } } From eb34de03e128bf356c440648d57d1b8615dc1671 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 13:24:49 +0200 Subject: [PATCH 17/44] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- broker/patron_request/service/message-handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index f4026e18..e40099b3 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -270,6 +270,7 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } case iso18626.TypeStatusRetryPossible: eventName = SupplierRetryConditional + setSupplierMessage(sam, &pr) if sam.DeliveryInfo != nil { retryItemId = sam.DeliveryInfo.ItemId } From 3bc091f263e36c93606f25cc3085f69d1aedb948 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 13:25:13 +0200 Subject: [PATCH 18/44] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- misc/returnables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/returnables.yaml b/misc/returnables.yaml index 13dd159b..2b576dbf 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -104,7 +104,7 @@ states: - name: RETRY_PENDING display: Retry Pending - desc: Received supplier retry request with new conditions after initial conditions were rejected + desc: Received ISO18626 RetryPossible from supplier (may include updated metadata/conditions) side: REQUESTER primaryAction: accept-retry needsAttention: true From 3f13e333f323f93812eed56fe9cb94ce267018eb Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 13:25:25 +0200 Subject: [PATCH 19/44] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- broker/migrations/043_add_patron_links.down.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/migrations/043_add_patron_links.down.sql b/broker/migrations/043_add_patron_links.down.sql index 6453b513..f63a1fe1 100644 --- a/broker/migrations/043_add_patron_links.down.sql +++ b/broker/migrations/043_add_patron_links.down.sql @@ -1,3 +1,4 @@ +DROP VIEW IF EXISTS patron_request_search_view; ALTER TABLE patron_request DROP COLUMN next_req_id; ALTER TABLE patron_request DROP COLUMN prev_req_id; From fdeb194648e49a2685214b6b18d419653091a01f Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 13:30:54 +0200 Subject: [PATCH 20/44] has_internal_note --- broker/migrations/043_add_patron_links.down.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/broker/migrations/043_add_patron_links.down.sql b/broker/migrations/043_add_patron_links.down.sql index f63a1fe1..074af5ce 100644 --- a/broker/migrations/043_add_patron_links.down.sql +++ b/broker/migrations/043_add_patron_links.down.sql @@ -16,6 +16,7 @@ SELECT WHERE n.pr_id = pr.id and cost is not null ) AS has_cost, (unread.unread_notifications_count > 0) AS has_unread_notification, + (pr.internal_note IS NOT NULL AND btrim(pr.internal_note) <> '') AS has_internal_note, pr.ill_request -> 'serviceInfo' ->> 'serviceType' AS service_type, pr.ill_request -> 'serviceInfo' -> 'serviceLevel' ->> '#text' AS service_level, immutable_to_timestamp(pr.ill_request -> 'serviceInfo' ->> 'needBeforeDate') AS needed_at, From 317fcb3c04a8b0b9167d2f166b7478fced35ff32 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 13:31:15 +0200 Subject: [PATCH 21/44] update --- broker/patron_request/service/statemodels/returnables.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/patron_request/service/statemodels/returnables.json b/broker/patron_request/service/statemodels/returnables.json index 5d447604..8dadb023 100644 --- a/broker/patron_request/service/statemodels/returnables.json +++ b/broker/patron_request/service/statemodels/returnables.json @@ -157,7 +157,7 @@ { "name": "RETRY_PENDING", "display": "Retry Pending", - "desc": "Received supplier retry request with new conditions after initial conditions were rejected", + "desc": "Received ISO18626 RetryPossible from supplier (may include updated metadata/conditions)", "side": "REQUESTER", "primaryAction": "accept-retry", "needsAttention": true, From ff241ea5d52506dc75b6aad3f7d9fa5508bdf93c Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 15:03:59 +0200 Subject: [PATCH 22/44] migrations update --- ...43_add_patron_links.down.sql => 045_add_patron_links.down.sql} | 0 .../{043_add_patron_links.up.sql => 045_add_patron_links.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename broker/migrations/{043_add_patron_links.down.sql => 045_add_patron_links.down.sql} (100%) rename broker/migrations/{043_add_patron_links.up.sql => 045_add_patron_links.up.sql} (100%) diff --git a/broker/migrations/043_add_patron_links.down.sql b/broker/migrations/045_add_patron_links.down.sql similarity index 100% rename from broker/migrations/043_add_patron_links.down.sql rename to broker/migrations/045_add_patron_links.down.sql diff --git a/broker/migrations/043_add_patron_links.up.sql b/broker/migrations/045_add_patron_links.up.sql similarity index 100% rename from broker/migrations/043_add_patron_links.up.sql rename to broker/migrations/045_add_patron_links.up.sql From 846bac8b228089c17a1e1e1957b347a5e3f4d66f Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 8 Jun 2026 17:42:52 +0200 Subject: [PATCH 23/44] retry_item_id persisted --- broker/migrations/045_add_patron_links.down.sql | 1 + broker/migrations/045_add_patron_links.up.sql | 1 + broker/patron_request/db/prcql.go | 1 + broker/patron_request/service/action.go | 8 +++++++- broker/patron_request/service/message-handler.go | 7 ++++--- broker/sqlc/pr_query.sql | 7 ++++--- broker/sqlc/pr_schema.sql | 3 ++- broker/test/patron_request/api/api-handler_test.go | 14 +++++++++++++- 8 files changed, 33 insertions(+), 9 deletions(-) diff --git a/broker/migrations/045_add_patron_links.down.sql b/broker/migrations/045_add_patron_links.down.sql index 074af5ce..cb251bbe 100644 --- a/broker/migrations/045_add_patron_links.down.sql +++ b/broker/migrations/045_add_patron_links.down.sql @@ -1,6 +1,7 @@ DROP VIEW IF EXISTS patron_request_search_view; ALTER TABLE patron_request DROP COLUMN next_req_id; ALTER TABLE patron_request DROP COLUMN prev_req_id; +ALTER TABLE patron_request DROP COLUMN retry_item_id; CREATE VIEW patron_request_search_view AS SELECT diff --git a/broker/migrations/045_add_patron_links.up.sql b/broker/migrations/045_add_patron_links.up.sql index f9f31fe0..c08ef901 100644 --- a/broker/migrations/045_add_patron_links.up.sql +++ b/broker/migrations/045_add_patron_links.up.sql @@ -1,5 +1,6 @@ ALTER TABLE patron_request ADD COLUMN next_req_id VARCHAR; ALTER TABLE patron_request ADD COLUMN prev_req_id VARCHAR; +ALTER TABLE patron_request ADD COLUMN retry_item_id VARCHAR; DROP VIEW IF EXISTS patron_request_search_view; diff --git a/broker/patron_request/db/prcql.go b/broker/patron_request/db/prcql.go index deb9fd57..6182a34c 100644 --- a/broker/patron_request/db/prcql.go +++ b/broker/patron_request/db/prcql.go @@ -295,6 +295,7 @@ func (q *Queries) ListPatronRequestsCql(ctx context.Context, db DBTX, arg ListPa &i.PatronRequestSearchView.InternalNote, &i.PatronRequestSearchView.NextReqID, &i.PatronRequestSearchView.PrevReqID, + &i.PatronRequestSearchView.RetryItemID, &i.PatronRequestSearchView.HasNotification, &i.PatronRequestSearchView.HasCost, &i.PatronRequestSearchView.HasUnreadNotification, diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index c2d526de..d84318f8 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -563,16 +563,22 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte } ctx.Logger().Info("cloned patron request for retry", "IllRequest.BibliographicInfo.SupplierUniqueRecordId", clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId) clone.State = pr_db.PatronRequestState("VALIDATED") + clone.TerminalState = false clone.ID = uuid.NewString() clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} clone.PrevReqID = getDbTextPtr(&pr.ID) - pr.NextReqID = getDbTextPtr(&clone.ID) + if pr.RetryItemID.Valid { + ctx.Logger().Info("AD: setting SupplierUniqueRecordId for retry", "SupplierUniqueRecordId", pr.RetryItemID.String) + clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryItemID.String + } _, err = a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(clone)) if err != nil { status, result := logActionErrorAndReturnResult(ctx, "failed to create patron request for retry", err) return actionExecutionResult{status: status, result: result, pr: pr} } + pr.NextReqID = getDbTextPtr(&clone.ID) + ctx.Logger().Info("AD: created new patron request for retry", "new_pr_id", clone.ID, "prev_pr_id", pr.ID) return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index e40099b3..633651ba 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -292,9 +292,10 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } if retryItemId != "" { ctx.Logger().Info("AD: received retry possible status with retry item id", "retryItemId", retryItemId) - // needs to be stored in the patron request so it can be used if the requester accepts the retry offer - // could be a field on its own (extend the db schema) - updatedPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId = retryItemId + updatedPr.RetryItemID = pgtype.Text{ + String: retryItemId, + Valid: true, + } } return m.updatePatronRequestAndCreateSamResponse(ctx, updatedPr, sam, stateChanged, parentEventID) } diff --git a/broker/sqlc/pr_query.sql b/broker/sqlc/pr_query.sql index 76d749f3..cae33fb7 100644 --- a/broker/sqlc/pr_query.sql +++ b/broker/sqlc/pr_query.sql @@ -58,13 +58,14 @@ SET ill_request = $3, ill_response = $19, internal_note = $20, next_req_id = $21, - prev_req_id = $22 + prev_req_id = $22, + retry_item_id = $23 WHERE id = $1 AND created_at = $2 AND (updated_at is null OR updated_at = $18) RETURNING sqlc.embed(patron_request); -- name: CreatePatronRequest :one -INSERT INTO patron_request (id, created_at, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, items, language, terminal_state, updated_at, ill_response, internal_note, next_req_id, prev_req_id) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) +INSERT INTO patron_request (id, created_at, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, items, language, terminal_state, updated_at, ill_response, internal_note, next_req_id, prev_req_id, retry_item_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) RETURNING sqlc.embed(patron_request); -- name: UpdatePatronRequestInternalNote :exec diff --git a/broker/sqlc/pr_schema.sql b/broker/sqlc/pr_schema.sql index 6ae433d6..f2264334 100644 --- a/broker/sqlc/pr_schema.sql +++ b/broker/sqlc/pr_schema.sql @@ -22,7 +22,8 @@ CREATE TABLE patron_request ill_response jsonb NOT NULL DEFAULT '{}'::jsonb, internal_note TEXT, next_req_id VARCHAR, - prev_req_id VARCHAR + prev_req_id VARCHAR, + retry_item_id VARCHAR ); CREATE OR REPLACE FUNCTION get_next_hrid(prefix VARCHAR) RETURNS VARCHAR AS $$ diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 59a241da..91ed3515 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -907,7 +907,19 @@ func TestAcceptRetry(t *testing.T) { assert.Contains(t, string(respBytes), "Action accept-retry is not allowed for patron request") // send request for the new PR created by accept-retry - thisPrPath = basePath + "/" + *foundPr.NextReqId + newId := *foundPr.NextReqId + thisPrPath = basePath + "/" + newId + + // check cloned request + respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, newId, foundPr.Id) + assert.Equal(t, "send-request", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.Equal(t, "123456789", foundPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId) + action = proapi.ExecuteAction{ Action: "send-request", } From 614664cb6891a8278c7e38427e7f9ddd429c1375 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 12 Jun 2026 09:11:22 +0200 Subject: [PATCH 24/44] migrations merge --- ...45_add_patron_links.down.sql => 046_add_patron_links.down.sql} | 0 .../{045_add_patron_links.up.sql => 046_add_patron_links.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename broker/migrations/{045_add_patron_links.down.sql => 046_add_patron_links.down.sql} (100%) rename broker/migrations/{045_add_patron_links.up.sql => 046_add_patron_links.up.sql} (100%) diff --git a/broker/migrations/045_add_patron_links.down.sql b/broker/migrations/046_add_patron_links.down.sql similarity index 100% rename from broker/migrations/045_add_patron_links.down.sql rename to broker/migrations/046_add_patron_links.down.sql diff --git a/broker/migrations/045_add_patron_links.up.sql b/broker/migrations/046_add_patron_links.up.sql similarity index 100% rename from broker/migrations/045_add_patron_links.up.sql rename to broker/migrations/046_add_patron_links.up.sql From b281c106526238ceb7e6ff141f425a113e2f12c3 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Sun, 14 Jun 2026 15:47:03 +0200 Subject: [PATCH 25/44] retryItemId in API structure --- broker/oapi/open-api.yaml | 3 +++ broker/patron_request/api/api-handler.go | 1 + broker/patron_request/service/action.go | 1 + .../patron_request/api/api-handler_test.go | 18 ++++++++++++++---- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index 48d9980b..b8b05488 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -588,6 +588,9 @@ components: prevReqId: type: string description: ID of the previous patron request in the sequence + retryItemId: + type: string + description: ID of the item to retry the request for when in retrying state required: - id - createdAt diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index cf7ca13f..1fba9fd7 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -916,6 +916,7 @@ func toApiPatronRequest(r *http.Request, request pr_db.PatronRequestSearchView) InternalNote: toString(request.InternalNote), NextReqId: toString(request.NextReqID), PrevReqId: toString(request.PrevReqID), + RetryItemId: toString(request.RetryItemID), } if request.UpdatedAt.Valid { pr.UpdatedAt = &request.UpdatedAt.Time diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index d84318f8..55290d0b 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -567,6 +567,7 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte clone.ID = uuid.NewString() clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} clone.PrevReqID = getDbTextPtr(&pr.ID) + clone.RetryItemID = pgtype.Text{} // clear retry item id to avoid confusion, will be set as SupplierUniqueRecordId in the new request if needed if pr.RetryItemID.Valid { ctx.Logger().Info("AD: setting SupplierUniqueRecordId for retry", "SupplierUniqueRecordId", pr.RetryItemID.String) clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryItemID.String diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 91ed3515..d81044ec 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -825,7 +825,7 @@ func TestAcceptRetry(t *testing.T) { err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") - assert.Equal(t, *newPr.Id, foundPr.Id) + assert.Equal(t, id, foundPr.Id) assert.True(t, foundPr.State != "") assert.Equal(t, string(prservice.SideBorrowing), foundPr.Side) assert.Equal(t, *newPr.RequesterSymbol, *foundPr.RequesterSymbol) @@ -833,7 +833,7 @@ func TestAcceptRetry(t *testing.T) { assert.Equal(t, *newPr.Patron, *foundPr.Patron) assertPatronRequestIllRequest(t, foundPr.IllRequest, func(r iso18626.Request) { assert.Equal(t, "RETRY:NOTFOUNDASCITED", r.BibliographicInfo.SupplierUniqueRecordId) - assert.Equal(t, *newPr.Id, r.Header.RequestingAgencyRequestId) + assert.Equal(t, id, r.Header.RequestingAgencyRequestId) assert.False(t, r.Header.Timestamp.IsZero()) }) assert.Equal(t, "validate", *foundPr.LastAction) @@ -867,6 +867,7 @@ func TestAcceptRetry(t *testing.T) { assert.Nil(t, pResult.Message) respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + foundPr = proapi.PatronRequest{} err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, *newPr.Id, foundPr.Id) @@ -893,7 +894,9 @@ func TestAcceptRetry(t *testing.T) { assert.NoError(t, err, "failed to unmarshal patron request action result") assert.Equal(t, "SUCCESS", pResult.Result) + // check the original request is updated with retry info and new request is created respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + foundPr = proapi.PatronRequest{} err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, *newPr.Id, foundPr.Id) @@ -901,23 +904,30 @@ func TestAcceptRetry(t *testing.T) { assert.Equal(t, "success", *foundPr.LastActionOutcome) assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) assert.NotNil(t, foundPr.NextReqId, "got pr "+string(respBytes)) + assert.Nil(t, foundPr.PrevReqId, "got pr "+string(respBytes)) + assert.Equal(t, "123456789", *foundPr.RetryItemId) // accept again - should fail as the request state it terminated respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 400) assert.Contains(t, string(respBytes), "Action accept-retry is not allowed for patron request") - // send request for the new PR created by accept-retry + // check cloned request newId := *foundPr.NextReqId + assert.NotEqual(t, newId, id) + thisPrPath = basePath + "/" + newId - // check cloned request respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + foundPr = proapi.PatronRequest{} err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, newId, foundPr.Id) assert.Equal(t, "send-request", *foundPr.LastAction) assert.Equal(t, "success", *foundPr.LastActionOutcome) assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.Equal(t, id, *foundPr.PrevReqId) + assert.Nil(t, foundPr.NextReqId) + assert.Nil(t, foundPr.RetryItemId) assert.Equal(t, "123456789", foundPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId) action = proapi.ExecuteAction{ From 780e65a0c13d839d50eca673f4d78559aec18e57 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Sun, 14 Jun 2026 17:25:46 +0200 Subject: [PATCH 26/44] Set new patron request fields one by one --- broker/patron_request/service/action.go | 26 +++++++++---------- .../patron_request/api/api-handler_test.go | 6 ++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 55290d0b..c75ecb60 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -541,32 +541,30 @@ func (a *PatronRequestActionService) rejectRetryBorrowingRequest(pr pr_db.Patron return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } -func clonePatronRequest(pr pr_db.PatronRequest) (pr_db.PatronRequest, error) { - prJSON, err := json.Marshal(pr) - if err != nil { - return pr_db.PatronRequest{}, err - } - var clone pr_db.PatronRequest - if err = json.Unmarshal(prJSON, &clone); err != nil { - return pr_db.PatronRequest{}, err - } - return clone, nil -} - func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) actionExecutionResult { result := events.EventResult{} - clone, err := clonePatronRequest(pr) + clone := pr_db.PatronRequest{} + clone.Side = pr.Side + clone.RequesterSymbol = pr.RequesterSymbol + clone.SupplierSymbol = pr.SupplierSymbol + clone.Patron = pr.Patron + clone.Tenant = pr.Tenant + var err error + clone.IllRequest, err = deepCopyISO18626Request(pr.IllRequest) if err != nil { - status, result := logActionErrorAndReturnResult(ctx, "failed to clone patron request for retry", err) + status, result := logActionErrorAndReturnResult(ctx, "failed to clone IllRequest for retry", err) return actionExecutionResult{status: status, result: result, pr: pr} } ctx.Logger().Info("cloned patron request for retry", "IllRequest.BibliographicInfo.SupplierUniqueRecordId", clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId) clone.State = pr_db.PatronRequestState("VALIDATED") clone.TerminalState = false clone.ID = uuid.NewString() + clone.RequesterReqID = getDbTextPtr(&clone.ID) clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} clone.PrevReqID = getDbTextPtr(&pr.ID) + clone.Language = pr.Language + clone.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent clone.RetryItemID = pgtype.Text{} // clear retry item id to avoid confusion, will be set as SupplierUniqueRecordId in the new request if needed if pr.RetryItemID.Valid { ctx.Logger().Info("AD: setting SupplierUniqueRecordId for retry", "SupplierUniqueRecordId", pr.RetryItemID.String) diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index d81044ec..f10cfed4 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -922,9 +922,9 @@ func TestAcceptRetry(t *testing.T) { err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, newId, foundPr.Id) - assert.Equal(t, "send-request", *foundPr.LastAction) - assert.Equal(t, "success", *foundPr.LastActionOutcome) - assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.Nil(t, foundPr.LastAction) + assert.Nil(t, foundPr.LastActionOutcome) + assert.Nil(t, foundPr.LastActionResult) assert.Equal(t, id, *foundPr.PrevReqId) assert.Nil(t, foundPr.NextReqId) assert.Nil(t, foundPr.RetryItemId) From 776c9c2775975c011523e271f6a67052576fc55e Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 15 Jun 2026 10:42:49 +0200 Subject: [PATCH 27/44] unfilled as last; fix SupplierUniqueRecordId overwrite --- broker/handler/iso18626-handler.go | 11 +++++++++++ broker/patron_request/service/action.go | 2 -- broker/patron_request/service/message-handler.go | 1 - broker/test/patron_request/api/api-handler_test.go | 14 +++++++++----- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/broker/handler/iso18626-handler.go b/broker/handler/iso18626-handler.go index cd291c13..a42902d0 100644 --- a/broker/handler/iso18626-handler.go +++ b/broker/handler/iso18626-handler.go @@ -215,6 +215,17 @@ func handleRetryRequest(ctx common.ExtendedContext, request *iso18626.Request, r illTrans.Timestamp = timestamp _, err = repo.SaveIllTransaction(ctx, ill_db.SaveIllTransactionParams(illTrans)) + if err != nil { + return err + } + + // Keep the selected supplier's LocalID in sync with the updated SupplierUniqueRecordId. + // createRequestMessage overwrites BibliographicInfo.SupplierUniqueRecordId with LocalID, so + // without this update the stale LocalID from the original holdings lookup would be sent. + if newLocalId := request.BibliographicInfo.SupplierUniqueRecordId; newLocalId != "" { + selSup.LocalID = pgtype.Text{String: newLocalId, Valid: true} + _, err = repo.SaveLocatedSupplier(ctx, ill_db.SaveLocatedSupplierParams(selSup)) + } return err }) return id, err diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index c75ecb60..2ca481ee 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -567,7 +567,6 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte clone.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent clone.RetryItemID = pgtype.Text{} // clear retry item id to avoid confusion, will be set as SupplierUniqueRecordId in the new request if needed if pr.RetryItemID.Valid { - ctx.Logger().Info("AD: setting SupplierUniqueRecordId for retry", "SupplierUniqueRecordId", pr.RetryItemID.String) clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryItemID.String } @@ -577,7 +576,6 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte return actionExecutionResult{status: status, result: result, pr: pr} } pr.NextReqID = getDbTextPtr(&clone.ID) - ctx.Logger().Info("AD: created new patron request for retry", "new_pr_id", clone.ID, "prev_pr_id", pr.ID) return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} } diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index 633651ba..a35ce9ca 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -291,7 +291,6 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx return statusChangeNotAllowed() } if retryItemId != "" { - ctx.Logger().Info("AD: received retry possible status with retry item id", "retryItemId", retryItemId) updatedPr.RetryItemID = pgtype.Text{ String: retryItemId, Valid: true, diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index f10cfed4..fd077c90 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -930,6 +930,8 @@ func TestAcceptRetry(t *testing.T) { assert.Nil(t, foundPr.RetryItemId) assert.Equal(t, "123456789", foundPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId) + // retry 2nd time. The "123456789" item id should be kept as retryItemId + // illmock will use scenario unfilled action = proapi.ExecuteAction{ Action: "send-request", } @@ -945,12 +947,14 @@ func TestAcceptRetry(t *testing.T) { assert.Nil(t, pResult.Message) test.WaitForPredicateToBeTrue(func() bool { - respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) - return strings.Contains(string(respBytes), "\"name\":\"accept-retry\"") + respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) + foundPr = proapi.PatronRequest{} + err = json.Unmarshal(respBytes, &foundPr) + return err == nil && foundPr.State == "UNFILLED" && foundPr.TerminalState }) - respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) - // TODO - // assert.Contains(t, string(respBytes), "\"name\":\"accept-retry\"") + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, "UNFILLED", foundPr.State) + assert.True(t, foundPr.TerminalState) } func TestPostPatronRequestRejectsInvalidIllRequest(t *testing.T) { From d742ac3222b35a6880169a2f2ddc00fe2d48904e Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 15 Jun 2026 10:52:09 +0200 Subject: [PATCH 28/44] Revert go.work.sum --- go.work.sum | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/go.work.sum b/go.work.sum index d1a14d07..b2c6ca06 100644 --- a/go.work.sum +++ b/go.work.sum @@ -770,7 +770,6 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -855,7 +854,6 @@ github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= @@ -919,7 +917,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= @@ -973,7 +970,6 @@ github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170/go.mod github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= @@ -1010,6 +1006,7 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= @@ -1082,7 +1079,6 @@ github.com/tonistiigi/go-archvariant v1.0.0/go.mod h1:TxFmO5VS6vMq2kvs3ht04iPXtu github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -1104,7 +1100,6 @@ github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= From 6cbd997cdb5f31c0305db0879faa6d4a9a5ed826 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 15 Jun 2026 11:06:05 +0200 Subject: [PATCH 29/44] Use definition --- broker/patron_request/service/action.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 2ca481ee..0f8a001a 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -557,7 +557,7 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte return actionExecutionResult{status: status, result: result, pr: pr} } ctx.Logger().Info("cloned patron request for retry", "IllRequest.BibliographicInfo.SupplierUniqueRecordId", clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId) - clone.State = pr_db.PatronRequestState("VALIDATED") + clone.State = BorrowerStateValidated clone.TerminalState = false clone.ID = uuid.NewString() clone.RequesterReqID = getDbTextPtr(&clone.ID) From a13ff157d679e02b9008f0df40b4c8d40c960848 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 15 Jun 2026 12:23:16 +0200 Subject: [PATCH 30/44] ItemID --- broker/patron_request/service/action.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 0f8a001a..43538fd3 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -52,7 +52,7 @@ type actionParams struct { Currency string `json:"currency,omitempty"` ReasonUnfilled string `json:"reasonUnfilled,omitempty"` ReasonRetry string `json:"reasonRetry,omitempty"` - ItemId string `json:"itemId,omitempty"` + ItemID string `json:"itemId,omitempty"` } func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) *PatronRequestActionService { @@ -825,9 +825,9 @@ func (a *PatronRequestActionService) acceptCancelLenderRequest(ctx common.Extend func (a *PatronRequestActionService) askRetryLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, params actionParams) actionExecutionResult { var deliveryInfo *iso18626.DeliveryInfo - if params.ItemId != "" { + if params.ItemID != "" { deliveryInfo = &iso18626.DeliveryInfo{ - ItemId: params.ItemId, + ItemId: params.ItemID, } } reasonRetry := string(iso18626.ReasonRetryNotFoundAsCited) From aa1678c907aae2054e6404eeccec3ed78efe85c9 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 15 Jun 2026 12:23:30 +0200 Subject: [PATCH 31/44] Remove Info log --- broker/patron_request/service/action.go | 1 - 1 file changed, 1 deletion(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 43538fd3..9569e1d8 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -556,7 +556,6 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte status, result := logActionErrorAndReturnResult(ctx, "failed to clone IllRequest for retry", err) return actionExecutionResult{status: status, result: result, pr: pr} } - ctx.Logger().Info("cloned patron request for retry", "IllRequest.BibliographicInfo.SupplierUniqueRecordId", clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId) clone.State = BorrowerStateValidated clone.TerminalState = false clone.ID = uuid.NewString() From 48216b68a4f82f9cde205848f696edd92971e3ef Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 17 Jun 2026 11:55:52 +0200 Subject: [PATCH 32/44] retry_bib_info --- .../migrations/046_add_patron_links.down.sql | 2 +- broker/migrations/046_add_patron_links.up.sql | 2 +- broker/oapi/open-api.yaml | 6 ++--- broker/patron_request/api/api-handler.go | 24 ++++++++++++++++++- broker/patron_request/db/prcql.go | 2 +- broker/patron_request/service/action.go | 17 +++++++++---- .../patron_request/service/message-handler.go | 16 ++++++------- broker/sqlc/pr_query.sql | 4 ++-- broker/sqlc/pr_schema.sql | 2 +- broker/sqlc/sqlc.yaml | 10 ++++++++ .../patron_request/api/api-handler_test.go | 4 ++-- 11 files changed, 65 insertions(+), 24 deletions(-) diff --git a/broker/migrations/046_add_patron_links.down.sql b/broker/migrations/046_add_patron_links.down.sql index cb251bbe..a71a109d 100644 --- a/broker/migrations/046_add_patron_links.down.sql +++ b/broker/migrations/046_add_patron_links.down.sql @@ -1,7 +1,7 @@ DROP VIEW IF EXISTS patron_request_search_view; ALTER TABLE patron_request DROP COLUMN next_req_id; ALTER TABLE patron_request DROP COLUMN prev_req_id; -ALTER TABLE patron_request DROP COLUMN retry_item_id; +ALTER TABLE patron_request DROP COLUMN retry_bib_info; CREATE VIEW patron_request_search_view AS SELECT diff --git a/broker/migrations/046_add_patron_links.up.sql b/broker/migrations/046_add_patron_links.up.sql index c08ef901..303b22c9 100644 --- a/broker/migrations/046_add_patron_links.up.sql +++ b/broker/migrations/046_add_patron_links.up.sql @@ -1,6 +1,6 @@ ALTER TABLE patron_request ADD COLUMN next_req_id VARCHAR; ALTER TABLE patron_request ADD COLUMN prev_req_id VARCHAR; -ALTER TABLE patron_request ADD COLUMN retry_item_id VARCHAR; +ALTER TABLE patron_request ADD COLUMN retry_bib_info JSONB; DROP VIEW IF EXISTS patron_request_search_view; diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index b8b05488..fec3c0d7 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -588,9 +588,9 @@ components: prevReqId: type: string description: ID of the previous patron request in the sequence - retryItemId: - type: string - description: ID of the item to retry the request for when in retrying state + retryBibInfo: + type: object + description: Bibliographic information for the item to retry the request for when in retrying state required: - id - createdAt diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index 1fba9fd7..ed14a1e5 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -916,7 +916,7 @@ func toApiPatronRequest(r *http.Request, request pr_db.PatronRequestSearchView) InternalNote: toString(request.InternalNote), NextReqId: toString(request.NextReqID), PrevReqId: toString(request.PrevReqID), - RetryItemId: toString(request.RetryItemID), + RetryBibInfo: bibInfoToMap(request.RetryBibInfo), } if request.UpdatedAt.Valid { pr.UpdatedAt = &request.UpdatedAt.Time @@ -935,6 +935,28 @@ func toString(text pgtype.Text) *string { return value } +func retryBibInfoToItemId(info *iso18626.BibliographicInfo) *string { + if info == nil || info.SupplierUniqueRecordId == "" { + return nil + } + return &info.SupplierUniqueRecordId +} + +func bibInfoToMap(info *iso18626.BibliographicInfo) *map[string]interface{} { + if info == nil { + return nil + } + b, err := json.Marshal(info) + if err != nil { + return nil + } + var m map[string]interface{} + if err := json.Unmarshal(b, &m); err != nil { + return nil + } + return &m +} + func (a *PatronRequestApiHandler) parseAndValidateIllRequest( ctx common.ExtendedContext, request *proapi.CreatePatronRequest, diff --git a/broker/patron_request/db/prcql.go b/broker/patron_request/db/prcql.go index c0ad7af7..61d865e3 100644 --- a/broker/patron_request/db/prcql.go +++ b/broker/patron_request/db/prcql.go @@ -295,7 +295,7 @@ func (q *Queries) ListPatronRequestsCql(ctx context.Context, db DBTX, arg ListPa &i.PatronRequestSearchView.InternalNote, &i.PatronRequestSearchView.NextReqID, &i.PatronRequestSearchView.PrevReqID, - &i.PatronRequestSearchView.RetryItemID, + &i.PatronRequestSearchView.RetryBibInfo, &i.PatronRequestSearchView.HasNotification, &i.PatronRequestSearchView.HasCost, &i.PatronRequestSearchView.HasUnreadNotification, diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 9569e1d8..87ee270c 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -563,10 +563,19 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} clone.PrevReqID = getDbTextPtr(&pr.ID) clone.Language = pr.Language - clone.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent - clone.RetryItemID = pgtype.Text{} // clear retry item id to avoid confusion, will be set as SupplierUniqueRecordId in the new request if needed - if pr.RetryItemID.Valid { - clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryItemID.String + clone.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent + clone.RetryBibInfo = nil // clear retry bib info to avoid confusion + if pr.RetryBibInfo != nil { + // only take selected fields from retry bib info to allow for corrections without affecting other fields + if pr.RetryBibInfo.SupplierUniqueRecordId != "" { + clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryBibInfo.SupplierUniqueRecordId + } + if pr.RetryBibInfo.Title != "" { + clone.IllRequest.BibliographicInfo.Title = pr.RetryBibInfo.Title + } + if pr.RetryBibInfo.Author != "" { + clone.IllRequest.BibliographicInfo.Author = pr.RetryBibInfo.Author + } } _, err = a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(clone)) diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index a35ce9ca..475a7a86 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -229,7 +229,7 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } eventName := MessageEvent("") - retryItemId := "" + retryBibInfo := (*iso18626.BibliographicInfo)(nil) switch sam.StatusInfo.Status { case iso18626.TypeStatusExpectToSupply: eventName = SupplierExpectToSupply @@ -271,8 +271,11 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx case iso18626.TypeStatusRetryPossible: eventName = SupplierRetryConditional setSupplierMessage(sam, &pr) - if sam.DeliveryInfo != nil { - retryItemId = sam.DeliveryInfo.ItemId + // later, we can use MessageInfo.Note to pass bibliographic hints for the retry request + if sam.DeliveryInfo != nil && sam.DeliveryInfo.ItemId != "" { + retryBibInfo = &iso18626.BibliographicInfo{ + SupplierUniqueRecordId: sam.DeliveryInfo.ItemId, + } } } @@ -290,11 +293,8 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx if !eventDefined { return statusChangeNotAllowed() } - if retryItemId != "" { - updatedPr.RetryItemID = pgtype.Text{ - String: retryItemId, - Valid: true, - } + if retryBibInfo != nil { + updatedPr.RetryBibInfo = retryBibInfo } return m.updatePatronRequestAndCreateSamResponse(ctx, updatedPr, sam, stateChanged, parentEventID) } diff --git a/broker/sqlc/pr_query.sql b/broker/sqlc/pr_query.sql index cae33fb7..57d5be43 100644 --- a/broker/sqlc/pr_query.sql +++ b/broker/sqlc/pr_query.sql @@ -59,12 +59,12 @@ SET ill_request = $3, internal_note = $20, next_req_id = $21, prev_req_id = $22, - retry_item_id = $23 + retry_bib_info = $23 WHERE id = $1 AND created_at = $2 AND (updated_at is null OR updated_at = $18) RETURNING sqlc.embed(patron_request); -- name: CreatePatronRequest :one -INSERT INTO patron_request (id, created_at, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, items, language, terminal_state, updated_at, ill_response, internal_note, next_req_id, prev_req_id, retry_item_id) +INSERT INTO patron_request (id, created_at, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, items, language, terminal_state, updated_at, ill_response, internal_note, next_req_id, prev_req_id, retry_bib_info) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) RETURNING sqlc.embed(patron_request); diff --git a/broker/sqlc/pr_schema.sql b/broker/sqlc/pr_schema.sql index f2264334..864bb8ba 100644 --- a/broker/sqlc/pr_schema.sql +++ b/broker/sqlc/pr_schema.sql @@ -23,7 +23,7 @@ CREATE TABLE patron_request internal_note TEXT, next_req_id VARCHAR, prev_req_id VARCHAR, - retry_item_id VARCHAR + retry_bib_info JSONB ); CREATE OR REPLACE FUNCTION get_next_hrid(prefix VARCHAR) RETURNS VARCHAR AS $$ diff --git a/broker/sqlc/sqlc.yaml b/broker/sqlc/sqlc.yaml index 591fc5e4..c2ccdb26 100644 --- a/broker/sqlc/sqlc.yaml +++ b/broker/sqlc/sqlc.yaml @@ -104,6 +104,16 @@ sql: - column: "patron_request_search_view.items" go_type: type: "[]PrItem" + - column: "patron_request.retry_bib_info" + go_type: + import: "github.com/indexdata/crosslink/iso18626" + type: "BibliographicInfo" + pointer: true + - column: "patron_request_search_view.retry_bib_info" + go_type: + import: "github.com/indexdata/crosslink/iso18626" + type: "BibliographicInfo" + pointer: true - column: "notification.side" go_type: type: "PatronRequestSide" diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index fd077c90..d985d619 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -905,7 +905,7 @@ func TestAcceptRetry(t *testing.T) { assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) assert.NotNil(t, foundPr.NextReqId, "got pr "+string(respBytes)) assert.Nil(t, foundPr.PrevReqId, "got pr "+string(respBytes)) - assert.Equal(t, "123456789", *foundPr.RetryItemId) + assert.Equal(t, "123456789", (*foundPr.RetryBibInfo)["supplierUniqueRecordId"]) // accept again - should fail as the request state it terminated respBytes = httpRequest(t, "POST", thisPrPath+"/action"+queryParams, actionBytes, 400) @@ -927,7 +927,7 @@ func TestAcceptRetry(t *testing.T) { assert.Nil(t, foundPr.LastActionResult) assert.Equal(t, id, *foundPr.PrevReqId) assert.Nil(t, foundPr.NextReqId) - assert.Nil(t, foundPr.RetryItemId) + assert.Nil(t, foundPr.RetryBibInfo) assert.Equal(t, "123456789", foundPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId) // retry 2nd time. The "123456789" item id should be kept as retryItemId From 7db5fe957cead5659ad5d11839d21af508dae08c Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Wed, 17 Jun 2026 11:57:15 +0200 Subject: [PATCH 33/44] Unused function --- broker/patron_request/api/api-handler.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index ed14a1e5..502dc1a1 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -935,13 +935,6 @@ func toString(text pgtype.Text) *string { return value } -func retryBibInfoToItemId(info *iso18626.BibliographicInfo) *string { - if info == nil || info.SupplierUniqueRecordId == "" { - return nil - } - return &info.SupplierUniqueRecordId -} - func bibInfoToMap(info *iso18626.BibliographicInfo) *map[string]interface{} { if info == nil { return nil From 8b06cedadf7507ba6ec5162d71d05aefeb9476d6 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 12:57:07 +0200 Subject: [PATCH 34/44] Create as new; run actions --- broker/patron_request/service/action.go | 31 ++++++++++++------- .../patron_request/api/api-handler_test.go | 6 ++-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 40046ed3..4b578b28 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -31,9 +31,10 @@ type PatronRequestActionService struct { } type actionExecutionResult struct { - status events.EventStatus - result *events.EventResult - pr pr_db.PatronRequest + status events.EventStatus + result *events.EventResult + pr pr_db.PatronRequest + retryPr pr_db.PatronRequest } type autoActionFailure struct { @@ -188,9 +189,23 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended stateChanged = true } + if execResult.retryPr.ID != "" { + retryPr, err := a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(execResult.retryPr)) + if err != nil { + return logActionErrorAndReturnResult(ctx, "failed to create patron request for retry", err) + } + err = a.RunAutoActionsOnStateEntry(ctx, retryPr, &event.ID, event.EventData.User) + if err != nil { + return logActionErrorAndReturnResult(ctx, "failed to run auto actions on state entry", err) + } + } var err error updatedPr, err = a.prRepo.UpdatePatronRequest(ctx, pr_db.UpdatePatronRequestParams(updatedPr)) if err != nil { + // not the smartest approach but if the update fails we should clean up the retry request to avoid orphaned retries that the user can't do anything about + if execResult.retryPr.ID != "" { + a.prRepo.DeletePatronRequest(ctx, execResult.retryPr.ID) + } return logActionErrorAndReturnResult(ctx, "failed to update patron request", err) } @@ -595,7 +610,7 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte status, result := logActionErrorAndReturnResult(ctx, "failed to clone IllRequest for retry", err) return actionExecutionResult{status: status, result: result, pr: pr} } - clone.State = BorrowerStateValidated + clone.State = BorrowerStateNew clone.TerminalState = false clone.ID = uuid.NewString() clone.RequesterReqID = getDbTextPtr(&clone.ID) @@ -616,14 +631,8 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte clone.IllRequest.BibliographicInfo.Author = pr.RetryBibInfo.Author } } - - _, err = a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(clone)) - if err != nil { - status, result := logActionErrorAndReturnResult(ctx, "failed to create patron request for retry", err) - return actionExecutionResult{status: status, result: result, pr: pr} - } pr.NextReqID = getDbTextPtr(&clone.ID) - return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr} + return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr, retryPr: clone} } func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lms lms.LmsAdapter) actionExecutionResult { diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index d985d619..80bd8696 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -922,9 +922,9 @@ func TestAcceptRetry(t *testing.T) { err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, newId, foundPr.Id) - assert.Nil(t, foundPr.LastAction) - assert.Nil(t, foundPr.LastActionOutcome) - assert.Nil(t, foundPr.LastActionResult) + assert.Equal(t, "validate", *foundPr.LastAction) + assert.Equal(t, "success", *foundPr.LastActionOutcome) + assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) assert.Equal(t, id, *foundPr.PrevReqId) assert.Nil(t, foundPr.NextReqId) assert.Nil(t, foundPr.RetryBibInfo) From 5fb2369e89990f7fc0a0ce1d66bce0dabc48dc1e Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 12:59:10 +0200 Subject: [PATCH 35/44] check err --- broker/patron_request/service/action.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 4b578b28..f371d2d0 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -204,7 +204,10 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended if err != nil { // not the smartest approach but if the update fails we should clean up the retry request to avoid orphaned retries that the user can't do anything about if execResult.retryPr.ID != "" { - a.prRepo.DeletePatronRequest(ctx, execResult.retryPr.ID) + err := a.prRepo.DeletePatronRequest(ctx, execResult.retryPr.ID) + if err != nil { + ctx.Logger().Error("failed to delete retry patron request after update failure", "retry_pr_id", execResult.retryPr.ID, "error", err) + } } return logActionErrorAndReturnResult(ctx, "failed to update patron request", err) } From ee67aa889b8514a90465832d8b3f87ad0d509b12 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 13:17:06 +0200 Subject: [PATCH 36/44] one transaction --- broker/patron_request/service/action.go | 36 +++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index f371d2d0..925a58c0 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -189,29 +189,31 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended stateChanged = true } - if execResult.retryPr.ID != "" { - retryPr, err := a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(execResult.retryPr)) - if err != nil { - return logActionErrorAndReturnResult(ctx, "failed to create patron request for retry", err) + var createdRetryPr *pr_db.PatronRequest + err := a.prRepo.WithTxFunc(ctx, func(repo pr_db.PrRepo) error { + if execResult.retryPr.ID != "" { + retryPr, err := repo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(execResult.retryPr)) + if err != nil { + return fmt.Errorf("create retry patron request: %w", err) + } + createdRetryPr = &retryPr } - err = a.RunAutoActionsOnStateEntry(ctx, retryPr, &event.ID, event.EventData.User) + var err error + updatedPr, err = repo.UpdatePatronRequest(ctx, pr_db.UpdatePatronRequestParams(updatedPr)) if err != nil { - return logActionErrorAndReturnResult(ctx, "failed to run auto actions on state entry", err) + return fmt.Errorf("update patron request: %w", err) } - } - var err error - updatedPr, err = a.prRepo.UpdatePatronRequest(ctx, pr_db.UpdatePatronRequestParams(updatedPr)) + return nil + }) if err != nil { - // not the smartest approach but if the update fails we should clean up the retry request to avoid orphaned retries that the user can't do anything about - if execResult.retryPr.ID != "" { - err := a.prRepo.DeletePatronRequest(ctx, execResult.retryPr.ID) - if err != nil { - ctx.Logger().Error("failed to delete retry patron request after update failure", "retry_pr_id", execResult.retryPr.ID, "error", err) - } + return logActionErrorAndReturnResult(ctx, "failed to persist patron request", err) + } + if createdRetryPr != nil { + err := a.RunAutoActionsOnStateEntry(ctx, *createdRetryPr, &event.ID, event.EventData.User) + if err != nil { + return logActionErrorAndReturnResult(ctx, "failed to run auto actions on state entry", err) } - return logActionErrorAndReturnResult(ctx, "failed to update patron request", err) } - if stateChanged { err := a.RunAutoActionsOnStateEntry(ctx, updatedPr, &event.ID, event.EventData.User) if err != nil { From 406341695ba49288c891ae43a00067d0feac6f9d Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 13:20:47 +0200 Subject: [PATCH 37/44] slightly simpler --- broker/patron_request/service/action.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 925a58c0..f15dc7e8 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -189,16 +189,15 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended stateChanged = true } - var createdRetryPr *pr_db.PatronRequest + var retryPr pr_db.PatronRequest err := a.prRepo.WithTxFunc(ctx, func(repo pr_db.PrRepo) error { + var err error if execResult.retryPr.ID != "" { - retryPr, err := repo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(execResult.retryPr)) + retryPr, err = repo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(execResult.retryPr)) if err != nil { return fmt.Errorf("create retry patron request: %w", err) } - createdRetryPr = &retryPr } - var err error updatedPr, err = repo.UpdatePatronRequest(ctx, pr_db.UpdatePatronRequestParams(updatedPr)) if err != nil { return fmt.Errorf("update patron request: %w", err) @@ -208,8 +207,8 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended if err != nil { return logActionErrorAndReturnResult(ctx, "failed to persist patron request", err) } - if createdRetryPr != nil { - err := a.RunAutoActionsOnStateEntry(ctx, *createdRetryPr, &event.ID, event.EventData.User) + if retryPr.ID != "" { + err := a.RunAutoActionsOnStateEntry(ctx, retryPr, &event.ID, event.EventData.User) if err != nil { return logActionErrorAndReturnResult(ctx, "failed to run auto actions on state entry", err) } From 2906253c816f522ae93c73bff2b7c3464c794185 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 13:41:56 +0200 Subject: [PATCH 38/44] Wait longer for Postgres containers to start --- broker/cmd/archive/main_test.go | 2 +- broker/cmd/broker/main_test.go | 2 +- broker/test/api/api-handler_test.go | 2 +- broker/test/client/client_test.go | 2 +- broker/test/events/eventbus_test.go | 2 +- broker/test/handler/iso18626-handler_test.go | 2 +- broker/test/holdings/holdings_test.go | 2 +- broker/test/patron_request/api/api-handler_test.go | 2 +- broker/test/patron_request/db/prrepo_test.go | 2 +- broker/test/pullslip/api/api_handler_test.go | 2 +- broker/test/service/e2e_test.go | 2 +- broker/test/utils/utils.go | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/broker/cmd/archive/main_test.go b/broker/cmd/archive/main_test.go index 3b392d25..76bd893b 100644 --- a/broker/cmd/archive/main_test.go +++ b/broker/cmd/archive/main_test.go @@ -25,7 +25,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/cmd/broker/main_test.go b/broker/cmd/broker/main_test.go index df2335e9..e75e4c09 100644 --- a/broker/cmd/broker/main_test.go +++ b/broker/cmd/broker/main_test.go @@ -31,7 +31,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/api/api-handler_test.go b/broker/test/api/api-handler_test.go index 45a7bb24..12853201 100644 --- a/broker/test/api/api-handler_test.go +++ b/broker/test/api/api-handler_test.go @@ -58,7 +58,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/client/client_test.go b/broker/test/client/client_test.go index 7f929204..b68ccc44 100644 --- a/broker/test/client/client_test.go +++ b/broker/test/client/client_test.go @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/events/eventbus_test.go b/broker/test/events/eventbus_test.go index e4bf17c7..290b065f 100644 --- a/broker/test/events/eventbus_test.go +++ b/broker/test/events/eventbus_test.go @@ -41,7 +41,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/handler/iso18626-handler_test.go b/broker/test/handler/iso18626-handler_test.go index 74e5f781..f719b6e1 100644 --- a/broker/test/handler/iso18626-handler_test.go +++ b/broker/test/handler/iso18626-handler_test.go @@ -55,7 +55,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/holdings/holdings_test.go b/broker/test/holdings/holdings_test.go index 3bd3da71..a9d750f9 100644 --- a/broker/test/holdings/holdings_test.go +++ b/broker/test/holdings/holdings_test.go @@ -47,7 +47,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 80bd8696..c67fe0ec 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -52,7 +52,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/patron_request/db/prrepo_test.go b/broker/test/patron_request/db/prrepo_test.go index 4b24bc83..1a68caa4 100644 --- a/broker/test/patron_request/db/prrepo_test.go +++ b/broker/test/patron_request/db/prrepo_test.go @@ -39,7 +39,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/pullslip/api/api_handler_test.go b/broker/test/pullslip/api/api_handler_test.go index a76f2762..ec719ec3 100644 --- a/broker/test/pullslip/api/api_handler_test.go +++ b/broker/test/pullslip/api/api_handler_test.go @@ -43,7 +43,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/service/e2e_test.go b/broker/test/service/e2e_test.go index b747f1fe..b9a47f53 100644 --- a/broker/test/service/e2e_test.go +++ b/broker/test/service/e2e_test.go @@ -42,7 +42,7 @@ func TestMain(m *testing.M) { postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) test.Expect(err, "failed to start db container") diff --git a/broker/test/utils/utils.go b/broker/test/utils/utils.go index d37ceeca..7e1a1e3f 100644 --- a/broker/test/utils/utils.go +++ b/broker/test/utils/utils.go @@ -90,7 +90,7 @@ func StartPGContainer() (context.Context, *postgres.PostgresContainer, string, e postgres.WithPassword("crosslink"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2).WithStartupTimeout(5*time.Second)), + WithOccurrence(2).WithStartupTimeout(30*time.Second)), ) if err != nil { return ctx, pgContainer, "", fmt.Errorf("failed to start db container: %w", err) From 22d8fcd813cabedcb6f2c614b221ea38ce6ba8e4 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 13:43:46 +0200 Subject: [PATCH 39/44] IllRequest headers align --- broker/patron_request/service/action.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index f15dc7e8..7c45a75e 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -619,6 +619,8 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte clone.ID = uuid.NewString() clone.RequesterReqID = getDbTextPtr(&clone.ID) clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} + clone.IllRequest.Header.RequestingAgencyRequestId = clone.ID + clone.IllRequest.Header.Timestamp = utils.XSDDateTime{Time: clone.CreatedAt.Time} clone.PrevReqID = getDbTextPtr(&pr.ID) clone.Language = pr.Language clone.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent From a8fd1f6b2479494982e34cc72aa683ea36e38979 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 13:46:02 +0200 Subject: [PATCH 40/44] rename --- broker/patron_request/service/action.go | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 7c45a75e..6d3e56cd 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -602,43 +602,43 @@ func (a *PatronRequestActionService) rejectRetryBorrowingRequest(pr pr_db.Patron func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) actionExecutionResult { result := events.EventResult{} - clone := pr_db.PatronRequest{} - clone.Side = pr.Side - clone.RequesterSymbol = pr.RequesterSymbol - clone.SupplierSymbol = pr.SupplierSymbol - clone.Patron = pr.Patron - clone.Tenant = pr.Tenant + retryPr := pr_db.PatronRequest{} + retryPr.Side = pr.Side + retryPr.RequesterSymbol = pr.RequesterSymbol + retryPr.SupplierSymbol = pr.SupplierSymbol + retryPr.Patron = pr.Patron + retryPr.Tenant = pr.Tenant var err error - clone.IllRequest, err = deepCopyISO18626Request(pr.IllRequest) + retryPr.IllRequest, err = deepCopyISO18626Request(pr.IllRequest) if err != nil { status, result := logActionErrorAndReturnResult(ctx, "failed to clone IllRequest for retry", err) return actionExecutionResult{status: status, result: result, pr: pr} } - clone.State = BorrowerStateNew - clone.TerminalState = false - clone.ID = uuid.NewString() - clone.RequesterReqID = getDbTextPtr(&clone.ID) - clone.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} - clone.IllRequest.Header.RequestingAgencyRequestId = clone.ID - clone.IllRequest.Header.Timestamp = utils.XSDDateTime{Time: clone.CreatedAt.Time} - clone.PrevReqID = getDbTextPtr(&pr.ID) - clone.Language = pr.Language - clone.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent - clone.RetryBibInfo = nil // clear retry bib info to avoid confusion + retryPr.State = BorrowerStateNew + retryPr.TerminalState = false + retryPr.ID = uuid.NewString() + retryPr.RequesterReqID = getDbTextPtr(&retryPr.ID) + retryPr.CreatedAt = pgtype.Timestamp{Valid: true, Time: time.Now()} + retryPr.IllRequest.Header.RequestingAgencyRequestId = retryPr.ID + retryPr.IllRequest.Header.Timestamp = utils.XSDDateTime{Time: retryPr.CreatedAt.Time} + retryPr.PrevReqID = getDbTextPtr(&pr.ID) + retryPr.Language = pr.Language + retryPr.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent + retryPr.RetryBibInfo = nil // clear retry bib info to avoid confusion if pr.RetryBibInfo != nil { // only take selected fields from retry bib info to allow for corrections without affecting other fields if pr.RetryBibInfo.SupplierUniqueRecordId != "" { - clone.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryBibInfo.SupplierUniqueRecordId + retryPr.IllRequest.BibliographicInfo.SupplierUniqueRecordId = pr.RetryBibInfo.SupplierUniqueRecordId } if pr.RetryBibInfo.Title != "" { - clone.IllRequest.BibliographicInfo.Title = pr.RetryBibInfo.Title + retryPr.IllRequest.BibliographicInfo.Title = pr.RetryBibInfo.Title } if pr.RetryBibInfo.Author != "" { - clone.IllRequest.BibliographicInfo.Author = pr.RetryBibInfo.Author + retryPr.IllRequest.BibliographicInfo.Author = pr.RetryBibInfo.Author } } - pr.NextReqID = getDbTextPtr(&clone.ID) - return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr, retryPr: clone} + pr.NextReqID = getDbTextPtr(&retryPr.ID) + return actionExecutionResult{status: events.EventStatusSuccess, result: &result, pr: pr, retryPr: retryPr} } func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lms lms.LmsAdapter) actionExecutionResult { From 7a46c1cc705f517e41b2b3544da1aa1d4b858e4d Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 13:54:47 +0200 Subject: [PATCH 41/44] CP --- broker/patron_request/service/action.go | 4 ++-- broker/patron_request/service/message-handler.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 6d3e56cd..8763d186 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -623,8 +623,8 @@ func (a *PatronRequestActionService) acceptRetryBorrowingRequest(ctx common.Exte retryPr.IllRequest.Header.Timestamp = utils.XSDDateTime{Time: retryPr.CreatedAt.Time} retryPr.PrevReqID = getDbTextPtr(&pr.ID) retryPr.Language = pr.Language - retryPr.Items = []pr_db.PrItem{} // items will be copied when the retry request is sent - retryPr.RetryBibInfo = nil // clear retry bib info to avoid confusion + retryPr.Items = []pr_db.PrItem{} + retryPr.RetryBibInfo = nil if pr.RetryBibInfo != nil { // only take selected fields from retry bib info to allow for corrections without affecting other fields if pr.RetryBibInfo.SupplierUniqueRecordId != "" { diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index 475a7a86..60d1e85b 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -229,7 +229,7 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx } eventName := MessageEvent("") - retryBibInfo := (*iso18626.BibliographicInfo)(nil) + var retryBibInfo *iso18626.BibliographicInfo switch sam.StatusInfo.Status { case iso18626.TypeStatusExpectToSupply: eventName = SupplierExpectToSupply From 7d40955c723f7e556f3029be164ee8917126dc29 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 14:18:37 +0200 Subject: [PATCH 42/44] CP --- broker/test/patron_request/api/api-handler_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index c67fe0ec..0773c15a 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -737,10 +737,10 @@ func TestRejectRetry(t *testing.T) { assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) // Wait until we can see possible action reject-retry - test.WaitForPredicateToBeTrue(func() bool { + assert.True(t, test.WaitForPredicateToBeTrue(func() bool { respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) return strings.Contains(string(respBytes), "\"name\":\"reject-retry\"") - }) + }), "reject-retry action did not appear in time") respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) assert.Contains(t, string(respBytes), "\"name\":\"reject-retry\"") @@ -876,10 +876,10 @@ func TestAcceptRetry(t *testing.T) { assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) // Wait until we can see possible action accept-retry - test.WaitForPredicateToBeTrue(func() bool { + assert.True(t, test.WaitForPredicateToBeTrue(func() bool { respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) return strings.Contains(string(respBytes), "\"name\":\"accept-retry\"") - }) + }), "accept-retry action did not appear in time") respBytes = httpRequest(t, "GET", thisPrPath+"/actions"+queryParams, []byte{}, 200) assert.Contains(t, string(respBytes), "\"name\":\"accept-retry\"") @@ -946,12 +946,12 @@ func TestAcceptRetry(t *testing.T) { assert.Equal(t, "SENT", *pResult.ToState) assert.Nil(t, pResult.Message) - test.WaitForPredicateToBeTrue(func() bool { + assert.True(t, test.WaitForPredicateToBeTrue(func() bool { respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) foundPr = proapi.PatronRequest{} err = json.Unmarshal(respBytes, &foundPr) return err == nil && foundPr.State == "UNFILLED" && foundPr.TerminalState - }) + }), "patron request did not reach UNFILLED state in time") assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, "UNFILLED", foundPr.State) assert.True(t, foundPr.TerminalState) From 0fbc59f8b62fbac85d5b258157ee2451ea09c910 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 18:00:49 +0200 Subject: [PATCH 43/44] ask-retry also in WILL_SUPPLY state --- broker/patron_request/service/action_mapping_test.go | 9 +++++---- .../patron_request/service/statemodels/returnables.json | 7 +++++++ misc/returnables.yaml | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/broker/patron_request/service/action_mapping_test.go b/broker/patron_request/service/action_mapping_test.go index 7945e8f4..bcb356e7 100644 --- a/broker/patron_request/service/action_mapping_test.go +++ b/broker/patron_request/service/action_mapping_test.go @@ -28,7 +28,7 @@ func TestNewReturnableActionMapping(t *testing.T) { lenderStateActionMapping := map[pr_db.PatronRequestState][]PatronRequestAction{ LenderStateNew: {{actionName: LenderActionValidate, auto: true}}, LenderStateValidated: {{actionName: LenderActionWillSupply, auto: true}, {actionName: LenderActionCannotSupply}, {actionName: LenderActionAddCondition}, {actionName: LenderActionAskRetry}}, - LenderStateWillSupply: {{actionName: LenderActionAddCondition}, {actionName: LenderActionShip}, {actionName: LenderActionCannotSupply}}, + LenderStateWillSupply: {{actionName: LenderActionAddCondition}, {actionName: LenderActionShip}, {actionName: LenderActionCannotSupply}, {actionName: LenderActionAskRetry}}, LenderStateConditionPending: {{actionName: LenderActionAddCondition}, {actionName: LenderActionCannotSupply}}, LenderStateConditionAccepted: {{actionName: LenderActionAddCondition}, {actionName: LenderActionShip}, {actionName: LenderActionCannotSupply}}, LenderStateShippedReturn: {{actionName: LenderActionMarkReceived}}, @@ -41,9 +41,9 @@ func TestNewReturnableActionMapping(t *testing.T) { assert.NotNil(t, returnableActionMapping) - mapCompare(t, returnableActionMapping.borrowerStateActionMapping, borrowerStateActionMapping) + mapCompare(t, borrowerStateActionMapping, returnableActionMapping.borrowerStateActionMapping) - mapCompare(t, returnableActionMapping.lenderStateActionMapping, lenderStateActionMapping) + mapCompare(t, lenderStateActionMapping, returnableActionMapping.lenderStateActionMapping) } var actionMappingService = ActionMappingService{} @@ -94,7 +94,7 @@ func TestGetActionsForPatronRequest(t *testing.T) { listCompare(t, []pr_db.PatronRequestAction{BorrowerActionAcceptCondition, BorrowerActionRejectCondition}, mapping.GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideBorrowing, State: BorrowerStateConditionPending})) // Lender - listCompare(t, []pr_db.PatronRequestAction{LenderActionAddCondition, LenderActionCannotSupply, LenderActionShip}, mapping.GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply})) + listCompare(t, []pr_db.PatronRequestAction{LenderActionAddCondition, LenderActionCannotSupply, LenderActionShip, LenderActionAskRetry}, mapping.GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply})) listCompare(t, []pr_db.PatronRequestAction{LenderActionAddCondition, LenderActionCannotSupply}, mapping.GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateConditionPending})) listCompare(t, []pr_db.PatronRequestAction{LenderActionAddCondition, LenderActionCannotSupply, LenderActionShip}, mapping.GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateConditionAccepted})) listCompare(t, []pr_db.PatronRequestAction{}, mapping.GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateShipped})) @@ -145,6 +145,7 @@ func TestGetAllowedActionsForPatronRequest1(t *testing.T) { {Name: string(LenderActionAddCondition), Parameters: []string{"note", "loanCondition", "cost", "currency"}}, {Name: string(LenderActionShip), Parameters: []string{"note"}, Primary: &tt}, {Name: string(LenderActionCannotSupply), Parameters: []string{"note", "reasonUnfilled"}}, + {Name: string(LenderActionAskRetry), Parameters: []string{"note", "reasonRetry", "itemId"}}, }}, mapping.GetAllowedActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply})) } diff --git a/broker/patron_request/service/statemodels/returnables.json b/broker/patron_request/service/statemodels/returnables.json index 6b144af6..65619a26 100644 --- a/broker/patron_request/service/statemodels/returnables.json +++ b/broker/patron_request/service/statemodels/returnables.json @@ -438,6 +438,13 @@ "transitions": { "success": "UNFILLED" } + }, + { + "name": "ask-retry", + "desc": "Ask requester to retry with new ISO18626 metadata", + "transitions": { + "success": "COMPLETED_WITH_RETRY" + } } ], "events": [ diff --git a/misc/returnables.yaml b/misc/returnables.yaml index ca619d86..fb09ec96 100644 --- a/misc/returnables.yaml +++ b/misc/returnables.yaml @@ -300,6 +300,10 @@ states: desc: Indicate cannot supply transitions: success: UNFILLED + - name: ask-retry + desc: Ask requester to retry with new ISO18626 metadata + transitions: + success: COMPLETED_WITH_RETRY events: - name: cancel-request desc: Requester sends ISO Cancel From c959dc00c892e32e485c201eb2727262c9cb4bbd Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 19 Jun 2026 18:31:47 +0200 Subject: [PATCH 44/44] Failure on original PR when actions fail on retry PR --- broker/patron_request/service/action.go | 10 ++++- broker/patron_request/service/action_test.go | 40 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 8763d186..95c0cc5b 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -210,7 +210,15 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended if retryPr.ID != "" { err := a.RunAutoActionsOnStateEntry(ctx, retryPr, &event.ID, event.EventData.User) if err != nil { - return logActionErrorAndReturnResult(ctx, "failed to run auto actions on state entry", err) + failedAction := action + var autoErr *autoActionFailure + if errors.As(err, &autoErr) { + failedAction = autoErr.action + } + a.markActionChainFailure(ctx, updatedPr.ID, failedAction) + autoActionErr := err.Error() + execResult.result.ActionResult.ChildActionError = &autoActionErr + return execResult.status, execResult.result } } if stateChanged { diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index f3978ace..902df5cf 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -1199,6 +1199,46 @@ func TestHandleInvokeLenderActionAskRetryFull(t *testing.T) { } } +func TestHandleInvokeBorrowerActionAcceptRetryAutoActionCreateTaskError(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockEventBus.createTaskErr = errors.New("event bus error") + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(createLmsAdapterMockLog(), nil) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, mockEventBus, mockIso18626Handler, lmsCreator) + illRequest := iso18626.Request{} + initialPR := pr_db.PatronRequest{ + ID: patronRequestId, + IllRequest: illRequest, + State: BorrowerStateRetryPending, + Side: SideBorrowing, + RequesterSymbol: getDbText("ISIL:REQ1"), + SupplierSymbol: getDbText("ISIL:SUP1"), + } + + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(initialPR, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(initialPR, nil).Once() + + action := BorrowerActionAcceptRetry + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{ + ID: "invoke-accept-retry", + PatronRequestID: patronRequestId, + EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}, + }) + + assert.Equal(t, events.EventStatusSuccess, status) + if assert.NotNil(t, resultData.ActionResult) && assert.NotNil(t, resultData.ActionResult.ChildActionError) { + assert.Equal(t, "event bus error", *resultData.ActionResult.ChildActionError) + } + // The original PR (not the retry PR) should be marked as a chain failure. + assert.Equal(t, patronRequestId, mockPrRepo.savedPr.ID) + assert.True(t, mockPrRepo.savedPr.NeedsAttention) + assert.Equal(t, string(BorrowerActionValidate), mockPrRepo.savedPr.LastAction.String) + assert.Equal(t, ActionOutcomeFailure, mockPrRepo.savedPr.LastActionOutcome.String) + assert.Equal(t, string(events.EventStatusError), mockPrRepo.savedPr.LastActionResult.String) +} + func TestHandleInvokeLenderActionAddConditionMissingConditionAndCost(t *testing.T) { mockPrRepo := new(MockPrRepo) lmsCreator := new(MockLmsCreator)