Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 65 additions & 0 deletions broker/patron_request/service/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedCo
return a.acceptRetryBorrowingRequest(ctx, pr)
case BorrowerActionSendNotification:
return a.sendNotificationBorrowingRequest(ctx, pr, params)
case BorrowerActionCancelLocalSupply:
return a.cancelLocalBorrowingRequest(ctx, pr)
case BorrowerActionCannotSupplyLocally:
return a.cannotSupplyLocallyBorrowingRequest(ctx, pr, params)
case BorrowerActionFillLocally:
return a.fillLocallyBorrowingRequest(ctx, pr, lmsAdapter, illRequest, params)
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 @@ -704,6 +710,65 @@ func (a *PatronRequestActionService) sendNotificationBorrowingRequest(ctx common
return a.sendEmailNotification(ctx, pr, params, pr.RequesterSymbol.String)
}

func (a *PatronRequestActionService) cancelLocalBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) actionExecutionResult {
result := events.EventResult{}
status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result,
iso18626.MessageInfo{
ReasonForMessage: iso18626.TypeReasonForMessageStatusChange,
},
iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled},
nil)
return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr)
}

func (a *PatronRequestActionService) cannotSupplyLocallyBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, params actionParams) actionExecutionResult {
result := events.EventResult{}
var reasonUnfilled *iso18626.TypeSchemeValuePair
if params.ReasonUnfilled != "" {
reasonUnfilled = &iso18626.TypeSchemeValuePair{Text: params.ReasonUnfilled}
}
status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result,
iso18626.MessageInfo{
ReasonForMessage: iso18626.TypeReasonForMessageStatusChange,
Note: params.Note,
ReasonUnfilled: reasonUnfilled,
},
iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled},
nil)
return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr)
}

func (a *PatronRequestActionService) fillLocallyBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lmsAdapter lms.LmsAdapter, illRequest iso18626.Request, params actionParams) actionExecutionResult {
_, _, _, err := lmsAdapter.RequestItem(
pr.ID,
illRequest.BibliographicInfo.SupplierUniqueRecordId,
pr.Patron.String,
lmsAdapter.RequesterPickupLocation(),
lmsAdapter.ItemLocation(),
)
if err != nil {
status, result := logActionErrorAndReturnResult(ctx, "LMS RequestItem failed", err)
return actionExecutionResult{status: status, result: result, pr: pr}
}

completedStatus := iso18626.TypeStatusLoanCompleted
if illRequest.ServiceInfo != nil && illRequest.ServiceInfo.ServiceType == iso18626.TypeServiceTypeCopy {
completedStatus = iso18626.TypeStatusCopyCompleted
}
result := events.EventResult{}
status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result,
iso18626.MessageInfo{
ReasonForMessage: iso18626.TypeReasonForMessageStatusChange,
Note: params.Note,
},
iso18626.StatusInfo{Status: completedStatus},
nil)
if result.OutgoingMessage.SupplyingAgencyMessage != nil {
setSupplierMessage(*result.OutgoingMessage.SupplyingAgencyMessage, &pr)
}
return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, 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
1 change: 1 addition & 0 deletions broker/patron_request/service/action_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestNewReturnableActionMapping(t *testing.T) {
BorrowerStateCheckedIn: {{actionName: BorrowerActionShipReturn}},
BorrowerStateRetryPending: {{actionName: BorrowerActionAcceptRetry}, {actionName: BorrowerActionRejectRetry}},
BorrowerStateUnfilled: {{actionName: BorrowerActionSendNotification, auto: true}},
BorrowerStateLocalSupply: {{actionName: BorrowerActionFillLocally}, {actionName: BorrowerActionCancelLocalSupply}, {actionName: BorrowerActionCannotSupplyLocally}},
}

lenderStateActionMapping := map[pr_db.PatronRequestState][]PatronRequestAction{
Expand Down
148 changes: 148 additions & 0 deletions broker/patron_request/service/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2620,6 +2620,144 @@ func TestHandleInvokeActionLenderActionSendNotification_emailServiceNotReady(t *
assert.Equal(t, LenderStateValidated, mockPrRepo.savedPr.State)
}

func TestHandleInvokeBorrowerActionCancelLocalSupply(t *testing.T) {
mockPrRepo := new(MockPrRepo)
lmsCreator := new(MockLmsCreator)
lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(lms.CreateLmsAdapterMockOK(), nil)
mockIso18626Handler := new(MockIso18626Handler)
prAction := CreatePatronRequestActionService(mockPrRepo, new(IllRepoMock), *new(events.EventBus), mockIso18626Handler, lmsCreator, new(EmailSenderMock))
illRequest := iso18626.Request{}
mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{
ID: patronRequestId,
IllRequest: illRequest,
State: BorrowerStateLocalSupply,
Side: SideBorrowing,
SupplierSymbol: getDbText("ISIL:SUP1"),
RequesterSymbol: getDbText("ISIL:REQ1"),
RequesterReqID: getDbText("req-1"),
}, nil)
action := BorrowerActionCancelLocalSupply

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, BorrowerStateCancelled, mockPrRepo.savedPr.State)
assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage)
assert.Equal(t, iso18626.TypeReasonForMessageStatusChange, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonForMessage)
assert.Equal(t, iso18626.TypeStatusCancelled, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status)
assert.False(t, mockIso18626Handler.lastSupplyingAgencyMessage.Header.Timestamp.IsZero())
assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.AnswerYesNo)
}

func TestHandleInvokeBorrowerActionCannotSupplyLocally(t *testing.T) {
mockPrRepo := new(MockPrRepo)
lmsCreator := new(MockLmsCreator)
lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(lms.CreateLmsAdapterMockOK(), nil)
mockIso18626Handler := new(MockIso18626Handler)
prAction := CreatePatronRequestActionService(mockPrRepo, new(IllRepoMock), *new(events.EventBus), mockIso18626Handler, lmsCreator, new(EmailSenderMock))
illRequest := iso18626.Request{}
mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{
ID: patronRequestId,
IllRequest: illRequest,
State: BorrowerStateLocalSupply,
Side: SideBorrowing,
SupplierSymbol: getDbText("ISIL:SUP1"),
RequesterSymbol: getDbText("ISIL:REQ1"),
RequesterReqID: getDbText("req-1"),
}, nil)
action := BorrowerActionCannotSupplyLocally

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, BorrowerStateSent, mockPrRepo.savedPr.State)
assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage)
assert.Equal(t, iso18626.TypeReasonForMessageStatusChange, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonForMessage)
assert.Equal(t, iso18626.TypeStatusUnfilled, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status)
assert.False(t, mockIso18626Handler.lastSupplyingAgencyMessage.Header.Timestamp.IsZero())
}

func TestHandleInvokeBorrowerActionFillLocally(t *testing.T) {
tests := []struct {
name string
serviceType iso18626.TypeServiceType
manualAdapter bool
expectedStatus iso18626.TypeStatus
}{
{name: "loan", serviceType: iso18626.TypeServiceTypeLoan, expectedStatus: iso18626.TypeStatusLoanCompleted},
{name: "copy", serviceType: iso18626.TypeServiceTypeCopy, expectedStatus: iso18626.TypeStatusCopyCompleted},
{name: "NCIP disabled", serviceType: iso18626.TypeServiceTypeLoan, manualAdapter: true, expectedStatus: iso18626.TypeStatusLoanCompleted},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPrRepo := new(MockPrRepo)
lmsCreator := new(MockLmsCreator)
mockIso18626Handler := new(MockIso18626Handler)
illRequest := iso18626.Request{
BibliographicInfo: iso18626.BibliographicInfo{SupplierUniqueRecordId: "local-record-1"},
ServiceInfo: &iso18626.ServiceInfo{ServiceType: tt.serviceType},
}
pr := pr_db.PatronRequest{
ID: patronRequestId,
IllRequest: illRequest,
State: BorrowerStateLocalSupply,
Side: SideBorrowing,
Patron: getDbText("patron-1"),
RequesterSymbol: getDbText("ISIL:REQ1"),
SupplierSymbol: getDbText("ISIL:REQ1"),
RequesterReqID: getDbText("req-1"),
NeedsAttention: true,
}

var lmsAdapter lms.LmsAdapter
if tt.manualAdapter {
lmsAdapter = &lms.LmsAdapterManual{}
} else {
adapterMock := &mockLmsAdapter{
requesterPickupLocation: "pickup-1",
itemLocation: "item-location-1",
}
adapterMock.On("RequestItem", patronRequestId, "local-record-1", "patron-1", "pickup-1", "item-location-1").
Return("", "", "", nil)
lmsAdapter = adapterMock
}
lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(lmsAdapter, nil)
mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr, nil)
prAction := CreatePatronRequestActionService(mockPrRepo, new(IllRepoMock), *new(events.EventBus), mockIso18626Handler, lmsCreator, new(EmailSenderMock))
action := BorrowerActionFillLocally

status, resultData := prAction.handleInvokeAction(appCtx, events.Event{
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.ToState) {
assert.Equal(t, string(BorrowerStateCompleted), *resultData.ActionResult.ToState)
}
assert.Equal(t, BorrowerStateCompleted, mockPrRepo.savedPr.State)
assert.True(t, mockPrRepo.savedPr.TerminalState)
assert.False(t, mockPrRepo.savedPr.NeedsAttention)
if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) {
assert.Equal(t, tt.expectedStatus, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status)
assert.Equal(t, iso18626.TypeReasonForMessageStatusChange, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonForMessage)
}
if adapterMock, ok := lmsAdapter.(*mockLmsAdapter); ok {
adapterMock.AssertExpectations(t)
}
})
}
}

type MockEventBus struct {
mock.Mock
events.EventBus
Expand Down Expand Up @@ -3088,6 +3226,16 @@ func TestLoadReturnableStateModel(t *testing.T) {
type mockLmsAdapter struct {
mock.Mock
lms.LmsAdapterManual
requesterPickupLocation string
itemLocation string
}

func (m *mockLmsAdapter) RequesterPickupLocation() string {
return m.requesterPickupLocation
}

func (m *mockLmsAdapter) ItemLocation() string {
return m.itemLocation
}

func (m *mockLmsAdapter) CancelRequestItem(requestId string, userId string) error {
Expand Down
27 changes: 25 additions & 2 deletions broker/patron_request/service/message-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessage(ctx common.Ex
return m.handleSupplyingAgencyMessageWithParent(ctx, sam, pr, nil)
}

func isLocalSupply(pr pr_db.PatronRequest, supplierSymbol string) bool {
return pr.RequesterSymbol.Valid && pr.RequesterSymbol.String == supplierSymbol
}

func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx common.ExtendedContext, sam iso18626.SupplyingAgencyMessage, pr pr_db.PatronRequest, parentEventID *string) (events.EventStatus, *iso18626.ISO18626Message, error) {
unsupportedReason := func() (events.EventStatus, *iso18626.ISO18626Message, error) {
err := fmt.Errorf("unsupported reason for message: %s", sam.MessageInfo.ReasonForMessage)
Expand Down Expand Up @@ -227,12 +231,14 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
Valid: true,
}
}

eventName := MessageEvent("")
var retryBibInfo *iso18626.BibliographicInfo
switch sam.StatusInfo.Status {
case iso18626.TypeStatusExpectToSupply:
eventName = SupplierExpectToSupply
if isLocalSupply(pr, supSymbol) {
eventName = SupplierExpectToSupplyLocal
}
Comment thread
JanisSaldabols marked this conversation as resolved.
case iso18626.TypeStatusWillSupply:
if sam.MessageInfo.ReasonForMessage == iso18626.TypeReasonForMessageCancelResponse {
if sam.MessageInfo.AnswerYesNo != nil && *sam.MessageInfo.AnswerYesNo == iso18626.TypeYesNoY {
Expand All @@ -258,15 +264,23 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
eventName = SupplierLoaned
case iso18626.TypeStatusLoanCompleted, iso18626.TypeStatusCopyCompleted:
eventName = SupplierCompleted
if isLocalSupply(pr, supSymbol) {
eventName = SupplierCompletedLocal
}
case iso18626.TypeStatusUnfilled:
eventName = SupplierUnfilled
if isLocalSupply(pr, supSymbol) {
eventName = SupplierUnfilledLocal
}
case iso18626.TypeStatusCancelled:
// Cancellation transition is accepted only for cancel-response messages.
if sam.MessageInfo.ReasonForMessage == iso18626.TypeReasonForMessageCancelResponse {
if sam.MessageInfo.AnswerYesNo != nil && *sam.MessageInfo.AnswerYesNo == iso18626.TypeYesNoN {
return contradictoryCancelResponse()
}
eventName = SupplierCancelAccepted
} else if sam.MessageInfo.ReasonForMessage == iso18626.TypeReasonForMessageStatusChange &&
isLocalSupply(pr, supSymbol) {
eventName = SupplierCancelledLocal
}
case iso18626.TypeStatusRetryPossible:
eventName = SupplierRetryConditional
Expand Down Expand Up @@ -294,6 +308,15 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
if !eventDefined {
return statusChangeNotAllowed()
}
if stateChanged &&
(eventName == SupplierCompletedLocal ||
eventName == SupplierCancelledLocal ||
eventName == SupplierUnfilledLocal) {
ctx.Logger().Warn("ignoring transition configured for local audit event",
"event", eventName, "state", pr.State)
updatedPr = pr
stateChanged = false
}
if retryBibInfo != nil {
updatedPr.RetryBibInfo = retryBibInfo
}
Expand Down
Loading
Loading