From 78a5ce6aa9b732884ec5c3413a152b2786dc2fb7 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 27 Mar 2026 15:31:37 +0100 Subject: [PATCH 1/6] CROSSLINK-236 ActionParams relayed (but not tested yet) --- broker/patron_request/service/action.go | 104 +++++++++++++++++++----- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 4e31a652..380337bb 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "time" @@ -46,6 +47,14 @@ func (e *autoActionFailure) Error() string { return e.msg } +type ActionParams struct { + Note string `json:"note,omitempty"` + LoanCondition string `json:"loanCondition,omitempty"` + Cost *float64 `json:"cost,omitempty"` + Currency string `json:"currency,omitempty"` + ReasonUnfilled string `json:"reasonUnfilled,omitempty"` +} + func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) *PatronRequestActionService { return &PatronRequestActionService{ prRepo: prRepo, @@ -267,7 +276,7 @@ func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedCo } } -func (a *PatronRequestActionService) handleLenderAction(ctx common.ExtendedContext, action pr_db.PatronRequestAction, pr pr_db.PatronRequest, illRequest iso18626.Request, actionParams map[string]interface{}, eventID *string) actionExecutionResult { +func (a *PatronRequestActionService) handleLenderAction(ctx common.ExtendedContext, action pr_db.PatronRequestAction, pr pr_db.PatronRequest, illRequest iso18626.Request, actionCustomData map[string]any, eventID *string) actionExecutionResult { if !pr.SupplierSymbol.Valid { status, result := a.logErrorAndReturnResult(ctx, "missing supplier symbol", nil) return actionExecutionResult{status: status, result: result, pr: pr} @@ -291,19 +300,31 @@ func (a *PatronRequestActionService) handleLenderAction(ctx common.ExtendedConte ctx.Logger().Error("failed to create LMS log event", "error", createErr) } }) + + bytes, err := json.Marshal(actionCustomData) + if err != nil { + status, result := a.logErrorAndReturnResult(ctx, "failed to marshal action parameters", err) + return actionExecutionResult{status: status, result: result, pr: pr} + } + var actionParams ActionParams + err = json.Unmarshal(bytes, &actionParams) + if err != nil { + status, result := a.logErrorAndReturnResult(ctx, "failed to unmarshal action parameters", err) + return actionExecutionResult{status: status, result: result, pr: pr} + } switch action { case LenderActionValidate: return a.validateLenderRequest(ctx, pr, lms) case LenderActionWillSupply: - return a.willSupplyLenderRequest(ctx, pr, lms, illRequest) + return a.willSupplyLenderRequest(ctx, pr, lms, illRequest, actionParams) case LenderActionRejectCancel: return a.rejectCancelLenderRequest(ctx, pr) case LenderActionCannotSupply: - return a.cannotSupplyLenderRequest(ctx, pr) + return a.cannotSupplyLenderRequest(ctx, pr, actionParams) case LenderActionAddCondition: return a.addConditionsLenderRequest(ctx, pr, actionParams) case LenderActionShip: - return a.shipLenderRequest(ctx, pr, lms, illRequest) + return a.shipLenderRequest(ctx, pr, lms, illRequest, actionParams) case LenderActionMarkReceived: return a.markReceivedLenderRequest(ctx, pr, lms, illRequest) case LenderActionAcceptCancel: @@ -597,7 +618,7 @@ func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedCo return actionExecutionResult{status: events.EventStatusSuccess, pr: pr} } -func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lmsAdapter lms.LmsAdapter, illRequest iso18626.Request) actionExecutionResult { +func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lmsAdapter lms.LmsAdapter, illRequest iso18626.Request, actionParams ActionParams) actionExecutionResult { itemId := illRequest.BibliographicInfo.SupplierUniqueRecordId requestId := illRequest.Header.RequestingAgencyRequestId userId := lmsAdapter.InstitutionalPatron(pr.RequesterSymbol.String) @@ -625,28 +646,58 @@ func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.Extended return actionExecutionResult{status: status, result: result, pr: pr} } result := events.EventResult{} - status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}) + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, + iso18626.MessageInfo{ + ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, + Note: actionParams.Note, + }, + iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, + nil) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } -func (a *PatronRequestActionService) cannotSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) actionExecutionResult { +func (a *PatronRequestActionService) cannotSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams ActionParams) actionExecutionResult { result := events.EventResult{} - status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled}) + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, + iso18626.MessageInfo{ + ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, + Note: actionParams.Note, + ReasonUnfilled: &iso18626.TypeSchemeValuePair{Text: actionParams.ReasonUnfilled}, + }, + iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled}, + nil) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } -func (a *PatronRequestActionService) addConditionsLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams map[string]interface{}) actionExecutionResult { +func (a *PatronRequestActionService) addConditionsLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams ActionParams) actionExecutionResult { + // TODO supply condition, cost, currency in SAM + var offeredCosts *iso18626.TypeCosts + if actionParams.Cost != nil { + _, costBase, costExp := utils.ExtractDecimal(strconv.FormatFloat(*actionParams.Cost, 'f', -1, 64), -1) + offeredCosts = &iso18626.TypeCosts{ + CurrencyCode: iso18626.TypeSchemeValuePair{Text: actionParams.Currency}, + MonetaryValue: utils.XSDDecimal{Base: costBase, Exp: costExp}, + } + } + var deliveryInfo *iso18626.DeliveryInfo + if actionParams.LoanCondition != "" { + deliveryInfo = &iso18626.DeliveryInfo{ + LoanCondition: &iso18626.TypeSchemeValuePair{Text: actionParams.LoanCondition}, + } + } result := events.EventResult{} status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ ReasonForMessage: iso18626.TypeReasonForMessageNotification, - Note: shim.RESHARE_ADD_LOAN_CONDITION, // TODO add action params + Note: actionParams.Note, + OfferedCosts: offeredCosts, }, - iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}) + iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, + deliveryInfo) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } -func (a *PatronRequestActionService) shipLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lmsAdapter lms.LmsAdapter, illRequest iso18626.Request) actionExecutionResult { +func (a *PatronRequestActionService) shipLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lmsAdapter lms.LmsAdapter, illRequest iso18626.Request, actionParams ActionParams) actionExecutionResult { requestId := illRequest.Header.RequestingAgencyRequestId userId := lmsAdapter.InstitutionalPatron(pr.RequesterSymbol.String) externalReferenceValue := "" @@ -680,11 +731,15 @@ func (a *PatronRequestActionService) shipLenderRequest(ctx common.ExtendedContex } } } - note := encodeItemsNote(items) + note := encodeItemsNote(items) + actionParams.Note result := events.EventResult{} status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, - iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, Note: note}, - iso18626.StatusInfo{Status: iso18626.TypeStatusLoaned}) + iso18626.MessageInfo{ + ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, + Note: note, + }, + iso18626.StatusInfo{Status: iso18626.TypeStatusLoaned}, + nil) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } @@ -718,7 +773,11 @@ func (a *PatronRequestActionService) markReceivedLenderRequest(ctx common.Extend } } result := events.EventResult{} - status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusLoanCompleted}) + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, + iso18626.MessageInfo{ + ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, + iso18626.StatusInfo{Status: iso18626.TypeStatusLoanCompleted}, + nil) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } @@ -730,7 +789,8 @@ func (a *PatronRequestActionService) rejectCancelLenderRequest(ctx common.Extend ReasonForMessage: iso18626.TypeReasonForMessageCancelResponse, AnswerYesNo: &no, }, - iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}) + iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, + nil) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } @@ -742,11 +802,12 @@ func (a *PatronRequestActionService) acceptCancelLenderRequest(ctx common.Extend ReasonForMessage: iso18626.TypeReasonForMessageCancelResponse, AnswerYesNo: &yes, }, - iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled}) + iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled}, + nil) return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr) } -func (a *PatronRequestActionService) sendSupplyingAgencyMessage(ctx common.ExtendedContext, pr pr_db.PatronRequest, result *events.EventResult, messageInfo iso18626.MessageInfo, statusInfo iso18626.StatusInfo) (events.EventStatus, *events.EventResult, *int) { +func (a *PatronRequestActionService) sendSupplyingAgencyMessage(ctx common.ExtendedContext, pr pr_db.PatronRequest, result *events.EventResult, messageInfo iso18626.MessageInfo, statusInfo iso18626.StatusInfo, deliveryInfo *iso18626.DeliveryInfo) (events.EventStatus, *events.EventResult, *int) { if !pr.RequesterSymbol.Valid { status, eventResult := a.logErrorAndReturnResult(ctx, "missing requester symbol", nil) return status, eventResult, nil @@ -773,8 +834,9 @@ func (a *PatronRequestActionService) sendSupplyingAgencyMessage(ctx common.Exten RequestingAgencyRequestId: pr.RequesterReqID.String, SupplyingAgencyRequestId: pr.ID, }, - MessageInfo: messageInfo, - StatusInfo: statusInfo, + MessageInfo: messageInfo, + StatusInfo: statusInfo, + DeliveryInfo: deliveryInfo, }, } if illMessage.SupplyingAgencyMessage.StatusInfo.LastChange.IsZero() { From d13b4081f2be1d9afe405555c7cf42a12e62217e Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 27 Mar 2026 16:04:42 +0100 Subject: [PATCH 2/6] Testing --- broker/patron_request/service/action.go | 3 +- broker/patron_request/service/action_test.go | 51 ++++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 380337bb..a023f3bc 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -670,7 +670,6 @@ func (a *PatronRequestActionService) cannotSupplyLenderRequest(ctx common.Extend } func (a *PatronRequestActionService) addConditionsLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams ActionParams) actionExecutionResult { - // TODO supply condition, cost, currency in SAM var offeredCosts *iso18626.TypeCosts if actionParams.Cost != nil { _, costBase, costExp := utils.ExtractDecimal(strconv.FormatFloat(*actionParams.Cost, 'f', -1, 64), -1) @@ -731,7 +730,7 @@ func (a *PatronRequestActionService) shipLenderRequest(ctx common.ExtendedContex } } } - note := encodeItemsNote(items) + actionParams.Note + note := actionParams.Note + encodeItemsNote(items) result := events.EventResult{} status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index c8f8bf4d..a74a6d70 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -841,6 +841,13 @@ func TestHandleInvokeLenderActionWillSupplyUseIllTitleWhenRequestItemEmptyOK(t * assert.Equal(t, "1", mockPrRepo.savedItems[0].Barcode) assert.Equal(t, "2", mockPrRepo.savedItems[0].CallNumber.String) assert.Equal(t, "title1", mockPrRepo.savedItems[0].Title.String) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) + } } func TestHandleInvokeLenderActionWillSupplyUseRequestItemTitleWhenAvailableOK(t *testing.T) { @@ -854,7 +861,12 @@ func TestHandleInvokeLenderActionWillSupplyUseRequestItemTitleWhenAvailableOK(t illRequest := iso18626.Request{BibliographicInfo: iso18626.BibliographicInfo{Title: "title1"}} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionWillSupply - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{ + "note": "my note", + }, + }}) assert.Equal(t, events.EventStatusSuccess, status) assert.NotNil(t, resultData) @@ -863,6 +875,13 @@ func TestHandleInvokeLenderActionWillSupplyUseRequestItemTitleWhenAvailableOK(t assert.Equal(t, "1", mockPrRepo.savedItems[0].Barcode) assert.Equal(t, "2", mockPrRepo.savedItems[0].CallNumber.String) assert.Equal(t, "title2", mockPrRepo.savedItems[0].Title.String) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "my note", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) + } } func TestHandleInvokeLenderActionRejectCancel(t *testing.T) { @@ -960,11 +979,29 @@ func TestHandleInvokeLenderActionAddCondition(t *testing.T) { mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionAddCondition - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) - + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{ + "loanCondition": "my condition", + "note": "Condition note", + "cost": 12.34, + }, + }}) assert.Equal(t, events.EventStatusSuccess, status) assert.NotNil(t, resultData) assert.Equal(t, LenderStateConditionPending, mockPrRepo.savedPr.State) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "Condition note", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) { + assert.Equal(t, 1234, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.MonetaryValue.Base) + assert.Equal(t, 2, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.MonetaryValue.Exp) + } + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) { + assert.Equal(t, "my condition", mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo.LoanCondition.Text) + } + } } func TestHandleInvokeLenderActionShipOK(t *testing.T) { @@ -994,13 +1031,19 @@ func TestHandleInvokeLenderActionShipOK(t *testing.T) { action := LenderActionShip - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{ + "note": "my note", + }, + }}) assert.Equal(t, events.EventStatusSuccess, status) assert.NotNil(t, resultData) assert.Equal(t, LenderStateShipped, mockPrRepo.savedPr.State) assert.Len(t, mockPrRepo.savedItems, 0) if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusLoaned, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "my note#MultipleItems#\n1234||\n5678||\n#MultipleItemsEnd#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) assert.False(t, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.LastChange.IsZero()) if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) { assert.False(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo.DateSent.IsZero()) From 8e0ad4b95c81ff2c05ff86c72814a2947e596b0e Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 27 Mar 2026 16:10:27 +0100 Subject: [PATCH 3/6] Test currency too --- broker/patron_request/service/action_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index a74a6d70..4058bb23 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -985,6 +985,7 @@ func TestHandleInvokeLenderActionAddCondition(t *testing.T) { "loanCondition": "my condition", "note": "Condition note", "cost": 12.34, + "currency": "DKK", }, }}) assert.Equal(t, events.EventStatusSuccess, status) @@ -997,6 +998,7 @@ func TestHandleInvokeLenderActionAddCondition(t *testing.T) { if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) { assert.Equal(t, 1234, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.MonetaryValue.Base) assert.Equal(t, 2, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.MonetaryValue.Exp) + assert.Equal(t, "DKK", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.CurrencyCode.Text) } if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) { assert.Equal(t, "my condition", mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo.LoanCondition.Text) From 3a9cacc71ad257c601bdf8bffe33921dad84507c Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 27 Mar 2026 16:30:48 +0100 Subject: [PATCH 4/6] Testing, keep shim.RESHARE_ADD_LOAN_CONDITION --- broker/patron_request/service/action.go | 12 ++- broker/patron_request/service/action_test.go | 100 ++++++++++++++++++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index a023f3bc..3172538e 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -649,7 +649,7 @@ func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.Extended status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, - Note: actionParams.Note, + Note: actionParams.Note + shim.RESHARE_ADD_LOAN_CONDITION, }, iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, nil) @@ -658,11 +658,15 @@ func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.Extended func (a *PatronRequestActionService) cannotSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams ActionParams) actionExecutionResult { result := events.EventResult{} + var reasonUnfilled *iso18626.TypeSchemeValuePair + if actionParams.ReasonUnfilled != "" { + reasonUnfilled = &iso18626.TypeSchemeValuePair{Text: actionParams.ReasonUnfilled} + } status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, Note: actionParams.Note, - ReasonUnfilled: &iso18626.TypeSchemeValuePair{Text: actionParams.ReasonUnfilled}, + ReasonUnfilled: reasonUnfilled, }, iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled}, nil) @@ -672,6 +676,10 @@ func (a *PatronRequestActionService) cannotSupplyLenderRequest(ctx common.Extend func (a *PatronRequestActionService) addConditionsLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams ActionParams) actionExecutionResult { var offeredCosts *iso18626.TypeCosts if actionParams.Cost != nil { + if actionParams.Currency == "" { + status, result := a.logErrorAndReturnResult(ctx, "currency is required when cost is provided", nil) + return actionExecutionResult{status: status, result: result, pr: pr} + } _, costBase, costExp := utils.ExtractDecimal(strconv.FormatFloat(*actionParams.Cost, 'f', -1, 64), -1) offeredCosts = &iso18626.TypeCosts{ CurrencyCode: iso18626.TypeSchemeValuePair{Text: actionParams.Currency}, diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index 4058bb23..c9a68517 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -844,7 +844,7 @@ func TestHandleInvokeLenderActionWillSupplyUseIllTitleWhenRequestItemEmptyOK(t * if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) - assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Equal(t, "#ReShareAddLoanCondition#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) } @@ -878,7 +878,7 @@ func TestHandleInvokeLenderActionWillSupplyUseRequestItemTitleWhenAvailableOK(t if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) - assert.Equal(t, "my note", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Equal(t, "my note#ReShareAddLoanCondition#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) } @@ -962,14 +962,81 @@ func TestHandleInvokeLenderActionCannotSupply(t *testing.T) { 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 := LenderActionCannotSupply - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + }}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.NotNil(t, resultData) + assert.Equal(t, LenderStateUnfilled, mockPrRepo.savedPr.State) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusUnfilled, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonUnfilled) + } +} + +func TestHandleInvokeLenderActionCannotSupplyWithReason(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 := LenderActionCannotSupply + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{ + "note": "my note", + "reasonUnfilled": "my reason", + }, + }}) assert.Equal(t, events.EventStatusSuccess, status) assert.NotNil(t, resultData) assert.Equal(t, LenderStateUnfilled, mockPrRepo.savedPr.State) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusUnfilled, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "my note", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonUnfilled) { + assert.Equal(t, "my reason", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonUnfilled.Text) + } + } +} + +func TestHandleInvokeLenderActionAddConditionOK(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 := LenderActionAddCondition + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + }}) + assert.Equal(t, events.EventStatusSuccess, status) + assert.NotNil(t, resultData) + assert.Equal(t, LenderStateConditionPending, mockPrRepo.savedPr.State) + + if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { + assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) + assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) + } } -func TestHandleInvokeLenderActionAddCondition(t *testing.T) { +func TestHandleInvokeLenderActionAddConditionWithCurrency(t *testing.T) { mockPrRepo := new(MockPrRepo) lmsCreator := new(MockLmsCreator) lmsCreator.On("GetAdapter", "ISIL:SUP1").Return(lms.CreateLmsAdapterMockOK(), nil) @@ -1006,6 +1073,31 @@ func TestHandleInvokeLenderActionAddCondition(t *testing.T) { } } +func TestHandleInvokeLenderActionAddConditionMissingCurrency(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 := LenderActionAddCondition + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{ + CommonEventData: events.CommonEventData{Action: &action}, + CustomData: map[string]any{ + "loanCondition": "my condition", + "note": "Condition note", + "cost": 12.34, + }, + }}) + assert.Equal(t, events.EventStatusError, status) + assert.NotNil(t, resultData) + assert.Equal(t, LenderStateValidated, mockPrRepo.savedPr.State) + assert.Equal(t, "currency is required when cost is provided", resultData.EventError.Message) + assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage) +} + func TestHandleInvokeLenderActionShipOK(t *testing.T) { mockPrRepo := new(MockPrRepo) lmsCreator := new(MockLmsCreator) From 286a457837522c8de62cb43bd776627cee81658d Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 27 Mar 2026 16:38:06 +0100 Subject: [PATCH 5/6] Fix shim.RESHARE_ADD_LOAN_CONDITION usage --- broker/patron_request/service/action.go | 4 ++-- broker/patron_request/service/action_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 3172538e..b8ca65ab 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -649,7 +649,7 @@ func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.Extended status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, - Note: actionParams.Note + shim.RESHARE_ADD_LOAN_CONDITION, + Note: actionParams.Note, }, iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, nil) @@ -696,7 +696,7 @@ func (a *PatronRequestActionService) addConditionsLenderRequest(ctx common.Exten status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ ReasonForMessage: iso18626.TypeReasonForMessageNotification, - Note: actionParams.Note, + Note: actionParams.Note + shim.RESHARE_ADD_LOAN_CONDITION, OfferedCosts: offeredCosts, }, iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index c9a68517..24c361d3 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -844,7 +844,7 @@ func TestHandleInvokeLenderActionWillSupplyUseIllTitleWhenRequestItemEmptyOK(t * if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) - assert.Equal(t, "#ReShareAddLoanCondition#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) } @@ -878,7 +878,7 @@ func TestHandleInvokeLenderActionWillSupplyUseRequestItemTitleWhenAvailableOK(t if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) - assert.Equal(t, "my note#ReShareAddLoanCondition#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Equal(t, "my note", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) } @@ -1030,7 +1030,7 @@ func TestHandleInvokeLenderActionAddConditionOK(t *testing.T) { if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) - assert.Equal(t, "", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Equal(t, "#ReShareAddLoanCondition#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.DeliveryInfo) } @@ -1061,7 +1061,7 @@ func TestHandleInvokeLenderActionAddConditionWithCurrency(t *testing.T) { if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) { assert.Equal(t, iso18626.TypeStatusWillSupply, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status) - assert.Equal(t, "Condition note", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) + assert.Equal(t, "Condition note#ReShareAddLoanCondition#", mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.Note) if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts) { assert.Equal(t, 1234, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.MonetaryValue.Base) assert.Equal(t, 2, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.OfferedCosts.MonetaryValue.Exp) From 12914d6d1e100623e8af009d30c050230068999a Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 27 Mar 2026 17:44:51 +0100 Subject: [PATCH 6/6] MapToStruct --- broker/common/common.go | 8 +++++ broker/common/common_test.go | 40 +++++++++++++++++++++++++ broker/patron_request/service/action.go | 7 +---- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/broker/common/common.go b/broker/common/common.go index cd60018c..f8c6ba7e 100644 --- a/broker/common/common.go +++ b/broker/common/common.go @@ -40,6 +40,14 @@ func StructToMap(obj any) (map[string]any, error) { return result, nil } +func MapToStruct(obj map[string]any, v any) error { + b, err := json.Marshal(obj) + if err != nil { + return err + } + return json.Unmarshal(b, v) +} + func UnpackItemsNote(note string) ([][]string, int, int) { startIdx := strings.Index(note, MULTIPLE_ITEMS) endIdx := strings.Index(note, MULTIPLE_ITEMS_END) diff --git a/broker/common/common_test.go b/broker/common/common_test.go index cfaab9db..d8021778 100644 --- a/broker/common/common_test.go +++ b/broker/common/common_test.go @@ -85,6 +85,46 @@ func TestStructToMap(t *testing.T) { } } +func TestMapToStruct(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + want User + wantErr bool + }{ + { + name: "Basic map conversion", + input: map[string]interface{}{ + "id": float64(1), + "name": "Alice", + "Active": true, + }, + want: User{ID: 1, Name: &alice, Active: true}, + wantErr: false, + }, + { + name: "42 as string instead of int", + input: map[string]interface{}{"id": "42"}, + want: User{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got User + err := MapToStruct(tt.input, &got) + if (err != nil) != tt.wantErr { + t.Errorf("MapToStruct() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MapToStruct() got = %v, want %v", got, tt.want) + } + }) + } +} + func TestUnpackItemsNote(t *testing.T) { // Just ID note := MULTIPLE_ITEMS + "\n1\n" + MULTIPLE_ITEMS_END diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index b8ca65ab..e4c5c003 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -301,13 +301,8 @@ func (a *PatronRequestActionService) handleLenderAction(ctx common.ExtendedConte } }) - bytes, err := json.Marshal(actionCustomData) - if err != nil { - status, result := a.logErrorAndReturnResult(ctx, "failed to marshal action parameters", err) - return actionExecutionResult{status: status, result: result, pr: pr} - } var actionParams ActionParams - err = json.Unmarshal(bytes, &actionParams) + err = common.MapToStruct(actionCustomData, &actionParams) if err != nil { status, result := a.logErrorAndReturnResult(ctx, "failed to unmarshal action parameters", err) return actionExecutionResult{status: status, result: result, pr: pr}