Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
febe0f2
CROSSLINK-264 ask-retry action
adamdickmeiss May 29, 2026
ac9170a
RETRY_REQUESTED state
adamdickmeiss May 29, 2026
d2f057b
Merge branch 'main' into CROSSLINK-264-ask-retry-action
adamdickmeiss Jun 1, 2026
67708e4
Update state model for retry
adamdickmeiss Jun 1, 2026
fa8eb8c
Potential fix for pull request finding
adamdickmeiss Jun 1, 2026
920a937
Potential fix for pull request finding
adamdickmeiss Jun 1, 2026
ddb2333
update returnables.json
adamdickmeiss Jun 1, 2026
4f1c545
Implement ask-retry
adamdickmeiss Jun 2, 2026
e7bf570
Create retry-conditonal event
adamdickmeiss Jun 2, 2026
524a26a
illmock: RETRY:NOTFOUNDASCITED
adamdickmeiss Jun 2, 2026
9529072
reject-retry
adamdickmeiss Jun 2, 2026
0b7be58
illmock: sets DeliveryInfo.ItemId on RETRY:NOTFOUNDASCITED
adamdickmeiss Jun 2, 2026
d1f9912
acceptRetry begin
adamdickmeiss Jun 2, 2026
95586a0
Add {next,prev}_req_id
adamdickmeiss Jun 2, 2026
cb56c77
Merge remote-tracking branch 'origin/main' into CROSSLINK-264-ask-ret…
adamdickmeiss Jun 4, 2026
d6da9bb
Clone patronrequest WIP
adamdickmeiss Jun 8, 2026
217bfb7
lint
adamdickmeiss Jun 8, 2026
7544ce8
Potential fix for pull request finding
adamdickmeiss Jun 8, 2026
eb34de0
Potential fix for pull request finding
adamdickmeiss Jun 8, 2026
3bc091f
Potential fix for pull request finding
adamdickmeiss Jun 8, 2026
3f13e33
Potential fix for pull request finding
adamdickmeiss Jun 8, 2026
fdeb194
has_internal_note
adamdickmeiss Jun 8, 2026
317fcb3
update
adamdickmeiss Jun 8, 2026
a78c2a1
Merge remote-tracking branch 'origin/main' into CROSSLINK-264-ask-ret…
adamdickmeiss Jun 8, 2026
ff241ea
migrations update
adamdickmeiss Jun 8, 2026
846bac8
retry_item_id persisted
adamdickmeiss Jun 8, 2026
975772f
Merge branch 'main' into CROSSLINK-264-ask-retry-action
adamdickmeiss Jun 12, 2026
614664c
migrations merge
adamdickmeiss Jun 12, 2026
b281c10
retryItemId in API structure
adamdickmeiss Jun 14, 2026
780e65a
Set new patron request fields one by one
adamdickmeiss Jun 14, 2026
776c9c2
unfilled as last; fix SupplierUniqueRecordId overwrite
adamdickmeiss Jun 15, 2026
d742ac3
Revert go.work.sum
adamdickmeiss Jun 15, 2026
6cbd997
Use definition
adamdickmeiss Jun 15, 2026
a13ff15
ItemID
adamdickmeiss Jun 15, 2026
aa1678c
Remove Info log
adamdickmeiss Jun 15, 2026
48216b6
retry_bib_info
adamdickmeiss Jun 17, 2026
7db5fe9
Unused function
adamdickmeiss Jun 17, 2026
da4432d
Merge remote-tracking branch 'origin/main' into CROSSLINK-264-ask-ret…
adamdickmeiss Jun 19, 2026
4bb42cf
Merge remote-tracking branch 'origin/main' into CROSSLINK-264-ask-ret…
adamdickmeiss Jun 19, 2026
7c11f1e
Merge remote-tracking branch 'origin/main' into CROSSLINK-264-ask-ret…
adamdickmeiss Jun 19, 2026
8b06ced
Create as new; run actions
adamdickmeiss Jun 19, 2026
5fb2369
check err
adamdickmeiss Jun 19, 2026
ee67aa8
one transaction
adamdickmeiss Jun 19, 2026
4063416
slightly simpler
adamdickmeiss Jun 19, 2026
2906253
Wait longer for Postgres containers to start
adamdickmeiss Jun 19, 2026
22d8fcd
IllRequest headers align
adamdickmeiss Jun 19, 2026
a8fd1f6
rename
adamdickmeiss Jun 19, 2026
7a46c1c
CP
adamdickmeiss Jun 19, 2026
7d40955
CP
adamdickmeiss Jun 19, 2026
0fbc59f
ask-retry also in WILL_SUPPLY state
adamdickmeiss Jun 19, 2026
c959dc0
Failure on original PR when actions fail on retry PR
adamdickmeiss Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions broker/patron_request/service/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Comment thread
adamdickmeiss marked this conversation as resolved.

func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) *PatronRequestActionService {
Expand Down Expand Up @@ -275,6 +277,10 @@ 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)
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}
Expand Down Expand Up @@ -329,6 +335,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}
Expand Down Expand Up @@ -528,6 +536,17 @@ 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) 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)
Expand Down Expand Up @@ -772,6 +791,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}
Expand Down
5 changes: 3 additions & 2 deletions broker/patron_request/service/action_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
Expand Down Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions broker/patron_request/service/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions broker/patron_request/service/message-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
}

eventName := MessageEvent("")
retryItemId := ""
switch sam.StatusInfo.Status {
case iso18626.TypeStatusExpectToSupply:
eventName = SupplierExpectToSupply
Expand Down Expand Up @@ -267,6 +268,11 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
}
eventName = SupplierCancelAccepted
}
case iso18626.TypeStatusRetryPossible:
eventName = SupplierRetryConditional
if sam.DeliveryInfo != nil {
retryItemId = sam.DeliveryInfo.ItemId
}
Comment thread
adamdickmeiss marked this conversation as resolved.
}

if eventName == "" {
Expand All @@ -283,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
}
Comment thread
adamdickmeiss marked this conversation as resolved.
Outdated
Comment thread
adamdickmeiss marked this conversation as resolved.
Outdated
return m.updatePatronRequestAndCreateSamResponse(ctx, updatedPr, sam, stateChanged, parentEventID)
}

Expand Down
51 changes: 38 additions & 13 deletions broker/patron_request/service/statemodel_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 (
Expand All @@ -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"
Expand All @@ -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"
)
Comment thread
adamdickmeiss marked this conversation as resolved.

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"
SupplierRetryConditional MessageEvent = "retry-conditional"
RequesterCancelRequest MessageEvent = "cancel-request"
RequesterReceived MessageEvent = "received"
RequesterShippedReturn MessageEvent = "shipped-return"
RequesterCondAccepted MessageEvent = "conditions-accepted"
RequesterCondRejected MessageEvent = "condition-rejected"
)
Comment thread
adamdickmeiss marked this conversation as resolved.

func requesterBuiltInStates() []string {
Expand All @@ -104,6 +112,9 @@ func requesterBuiltInStates() []string {
string(BorrowerStateCompleted),
string(BorrowerStateCancelled),
string(BorrowerStateUnfilled),
string(BorrowerStateRetryAccepted),
string(BorrowerStateRetryRejected),
string(BorrowerStateRetryPending),
})
}

Expand All @@ -121,6 +132,7 @@ func supplierBuiltInStates() []string {
string(LenderStateCompleted),
string(LenderStateCancelled),
string(LenderStateUnfilled),
string(LenderStateCompletedWithRetry),
})
}

Expand Down Expand Up @@ -162,6 +174,14 @@ func requesterBuiltInActions() []proapi.ActionCapability {
Name: string(BorrowerActionShipReturn),
Parameters: []string{},
},
{
Name: string(BorrowerActionAcceptRetry),
Parameters: []string{},
},
{
Name: string(BorrowerActionRejectRetry),
Parameters: []string{},
},
}
}

Expand Down Expand Up @@ -211,6 +231,10 @@ func supplierBuiltInActions() []proapi.ActionCapability {
Name: string(LenderActionAcceptCancel),
Parameters: []string{},
},
{
Name: string(LenderActionAskRetry),
Parameters: []string{},
},
Comment thread
adamdickmeiss marked this conversation as resolved.
}
}

Expand All @@ -234,6 +258,7 @@ func supplierBuiltInMessageEvents() []string {
string(SupplierUnfilled),
string(SupplierCancelAccepted),
string(SupplierCancelRejected),
string(SupplierRetryConditional),
})
}

Expand Down
Loading
Loading