Skip to content

Commit eed9e04

Browse files
authored
Merge branch 'main' into show-owner
2 parents 6941b9a + 00acdaf commit eed9e04

8 files changed

Lines changed: 480 additions & 42 deletions

File tree

broker/patron_request/service/action.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,12 @@ func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedCo
369369
return a.acceptRetryBorrowingRequest(ctx, pr)
370370
case BorrowerActionSendNotification:
371371
return a.sendNotificationBorrowingRequest(ctx, pr, params)
372+
case BorrowerActionCancelLocalSupply:
373+
return a.cancelLocalBorrowingRequest(ctx, pr)
374+
case BorrowerActionCannotSupplyLocally:
375+
return a.cannotSupplyLocallyBorrowingRequest(ctx, pr, params)
376+
case BorrowerActionFillLocally:
377+
return a.fillLocallyBorrowingRequest(ctx, pr, lmsAdapter, illRequest, params)
372378
default:
373379
status, result := logActionErrorAndReturnResult(ctx, "borrower action "+string(action)+" is not implemented yet", errors.New("invalid action"))
374380
return actionExecutionResult{status: status, result: result, pr: pr}
@@ -704,6 +710,65 @@ func (a *PatronRequestActionService) sendNotificationBorrowingRequest(ctx common
704710
return a.sendEmailNotification(ctx, pr, params, pr.RequesterSymbol.String)
705711
}
706712

713+
func (a *PatronRequestActionService) cancelLocalBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) actionExecutionResult {
714+
result := events.EventResult{}
715+
status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result,
716+
iso18626.MessageInfo{
717+
ReasonForMessage: iso18626.TypeReasonForMessageStatusChange,
718+
},
719+
iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled},
720+
nil)
721+
return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr)
722+
}
723+
724+
func (a *PatronRequestActionService) cannotSupplyLocallyBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, params actionParams) actionExecutionResult {
725+
result := events.EventResult{}
726+
var reasonUnfilled *iso18626.TypeSchemeValuePair
727+
if params.ReasonUnfilled != "" {
728+
reasonUnfilled = &iso18626.TypeSchemeValuePair{Text: params.ReasonUnfilled}
729+
}
730+
status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result,
731+
iso18626.MessageInfo{
732+
ReasonForMessage: iso18626.TypeReasonForMessageStatusChange,
733+
Note: params.Note,
734+
ReasonUnfilled: reasonUnfilled,
735+
},
736+
iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled},
737+
nil)
738+
return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr)
739+
}
740+
741+
func (a *PatronRequestActionService) fillLocallyBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lmsAdapter lms.LmsAdapter, illRequest iso18626.Request, params actionParams) actionExecutionResult {
742+
_, _, _, err := lmsAdapter.RequestItem(
743+
pr.ID,
744+
illRequest.BibliographicInfo.SupplierUniqueRecordId,
745+
pr.Patron.String,
746+
lmsAdapter.RequesterPickupLocation(),
747+
lmsAdapter.ItemLocation(),
748+
)
749+
if err != nil {
750+
status, result := logActionErrorAndReturnResult(ctx, "LMS RequestItem failed", err)
751+
return actionExecutionResult{status: status, result: result, pr: pr}
752+
}
753+
754+
completedStatus := iso18626.TypeStatusLoanCompleted
755+
if illRequest.ServiceInfo != nil && illRequest.ServiceInfo.ServiceType == iso18626.TypeServiceTypeCopy {
756+
completedStatus = iso18626.TypeStatusCopyCompleted
757+
}
758+
result := events.EventResult{}
759+
status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result,
760+
iso18626.MessageInfo{
761+
ReasonForMessage: iso18626.TypeReasonForMessageStatusChange,
762+
Note: params.Note,
763+
},
764+
iso18626.StatusInfo{Status: completedStatus},
765+
nil)
766+
if result.OutgoingMessage.SupplyingAgencyMessage != nil {
767+
setSupplierMessage(*result.OutgoingMessage.SupplyingAgencyMessage, &pr)
768+
}
769+
return a.checkSupplyingResponse(status, eventResult, &result, httpStatus, pr)
770+
}
771+
707772
func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, lms lms.LmsAdapter) actionExecutionResult {
708773
institutionalPatron := lms.InstitutionalPatron(pr.RequesterSymbol.String)
709774
_, err := lms.LookupUser(institutionalPatron)

broker/patron_request/service/action_mapping_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestNewReturnableActionMapping(t *testing.T) {
2626
BorrowerStateCheckedIn: {{actionName: BorrowerActionShipReturn}},
2727
BorrowerStateRetryPending: {{actionName: BorrowerActionAcceptRetry}, {actionName: BorrowerActionRejectRetry}},
2828
BorrowerStateUnfilled: {{actionName: BorrowerActionSendNotification, auto: true}},
29+
BorrowerStateLocalSupply: {{actionName: BorrowerActionFillLocally}, {actionName: BorrowerActionCancelLocalSupply}, {actionName: BorrowerActionCannotSupplyLocally}},
2930
}
3031

3132
lenderStateActionMapping := map[pr_db.PatronRequestState][]PatronRequestAction{

broker/patron_request/service/action_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,6 +2620,144 @@ func TestHandleInvokeActionLenderActionSendNotification_emailServiceNotReady(t *
26202620
assert.Equal(t, LenderStateValidated, mockPrRepo.savedPr.State)
26212621
}
26222622

2623+
func TestHandleInvokeBorrowerActionCancelLocalSupply(t *testing.T) {
2624+
mockPrRepo := new(MockPrRepo)
2625+
lmsCreator := new(MockLmsCreator)
2626+
lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(lms.CreateLmsAdapterMockOK(), nil)
2627+
mockIso18626Handler := new(MockIso18626Handler)
2628+
prAction := CreatePatronRequestActionService(mockPrRepo, new(IllRepoMock), *new(events.EventBus), mockIso18626Handler, lmsCreator, new(EmailSenderMock))
2629+
illRequest := iso18626.Request{}
2630+
mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{
2631+
ID: patronRequestId,
2632+
IllRequest: illRequest,
2633+
State: BorrowerStateLocalSupply,
2634+
Side: SideBorrowing,
2635+
SupplierSymbol: getDbText("ISIL:SUP1"),
2636+
RequesterSymbol: getDbText("ISIL:REQ1"),
2637+
RequesterReqID: getDbText("req-1"),
2638+
}, nil)
2639+
action := BorrowerActionCancelLocalSupply
2640+
2641+
status, resultData := prAction.handleInvokeAction(appCtx, events.Event{
2642+
PatronRequestID: patronRequestId,
2643+
EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}},
2644+
})
2645+
2646+
assert.Equal(t, events.EventStatusSuccess, status)
2647+
assert.NotNil(t, resultData)
2648+
assert.Equal(t, BorrowerStateCancelled, mockPrRepo.savedPr.State)
2649+
assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage)
2650+
assert.Equal(t, iso18626.TypeReasonForMessageStatusChange, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonForMessage)
2651+
assert.Equal(t, iso18626.TypeStatusCancelled, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status)
2652+
assert.False(t, mockIso18626Handler.lastSupplyingAgencyMessage.Header.Timestamp.IsZero())
2653+
assert.Nil(t, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.AnswerYesNo)
2654+
}
2655+
2656+
func TestHandleInvokeBorrowerActionCannotSupplyLocally(t *testing.T) {
2657+
mockPrRepo := new(MockPrRepo)
2658+
lmsCreator := new(MockLmsCreator)
2659+
lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(lms.CreateLmsAdapterMockOK(), nil)
2660+
mockIso18626Handler := new(MockIso18626Handler)
2661+
prAction := CreatePatronRequestActionService(mockPrRepo, new(IllRepoMock), *new(events.EventBus), mockIso18626Handler, lmsCreator, new(EmailSenderMock))
2662+
illRequest := iso18626.Request{}
2663+
mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{
2664+
ID: patronRequestId,
2665+
IllRequest: illRequest,
2666+
State: BorrowerStateLocalSupply,
2667+
Side: SideBorrowing,
2668+
SupplierSymbol: getDbText("ISIL:SUP1"),
2669+
RequesterSymbol: getDbText("ISIL:REQ1"),
2670+
RequesterReqID: getDbText("req-1"),
2671+
}, nil)
2672+
action := BorrowerActionCannotSupplyLocally
2673+
2674+
status, resultData := prAction.handleInvokeAction(appCtx, events.Event{
2675+
PatronRequestID: patronRequestId,
2676+
EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}},
2677+
})
2678+
2679+
assert.Equal(t, events.EventStatusSuccess, status)
2680+
assert.NotNil(t, resultData)
2681+
assert.Equal(t, BorrowerStateSent, mockPrRepo.savedPr.State)
2682+
assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage)
2683+
assert.Equal(t, iso18626.TypeReasonForMessageStatusChange, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonForMessage)
2684+
assert.Equal(t, iso18626.TypeStatusUnfilled, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status)
2685+
assert.False(t, mockIso18626Handler.lastSupplyingAgencyMessage.Header.Timestamp.IsZero())
2686+
}
2687+
2688+
func TestHandleInvokeBorrowerActionFillLocally(t *testing.T) {
2689+
tests := []struct {
2690+
name string
2691+
serviceType iso18626.TypeServiceType
2692+
manualAdapter bool
2693+
expectedStatus iso18626.TypeStatus
2694+
}{
2695+
{name: "loan", serviceType: iso18626.TypeServiceTypeLoan, expectedStatus: iso18626.TypeStatusLoanCompleted},
2696+
{name: "copy", serviceType: iso18626.TypeServiceTypeCopy, expectedStatus: iso18626.TypeStatusCopyCompleted},
2697+
{name: "NCIP disabled", serviceType: iso18626.TypeServiceTypeLoan, manualAdapter: true, expectedStatus: iso18626.TypeStatusLoanCompleted},
2698+
}
2699+
2700+
for _, tt := range tests {
2701+
t.Run(tt.name, func(t *testing.T) {
2702+
mockPrRepo := new(MockPrRepo)
2703+
lmsCreator := new(MockLmsCreator)
2704+
mockIso18626Handler := new(MockIso18626Handler)
2705+
illRequest := iso18626.Request{
2706+
BibliographicInfo: iso18626.BibliographicInfo{SupplierUniqueRecordId: "local-record-1"},
2707+
ServiceInfo: &iso18626.ServiceInfo{ServiceType: tt.serviceType},
2708+
}
2709+
pr := pr_db.PatronRequest{
2710+
ID: patronRequestId,
2711+
IllRequest: illRequest,
2712+
State: BorrowerStateLocalSupply,
2713+
Side: SideBorrowing,
2714+
Patron: getDbText("patron-1"),
2715+
RequesterSymbol: getDbText("ISIL:REQ1"),
2716+
SupplierSymbol: getDbText("ISIL:REQ1"),
2717+
RequesterReqID: getDbText("req-1"),
2718+
NeedsAttention: true,
2719+
}
2720+
2721+
var lmsAdapter lms.LmsAdapter
2722+
if tt.manualAdapter {
2723+
lmsAdapter = &lms.LmsAdapterManual{}
2724+
} else {
2725+
adapterMock := &mockLmsAdapter{
2726+
requesterPickupLocation: "pickup-1",
2727+
itemLocation: "item-location-1",
2728+
}
2729+
adapterMock.On("RequestItem", patronRequestId, "local-record-1", "patron-1", "pickup-1", "item-location-1").
2730+
Return("", "", "", nil)
2731+
lmsAdapter = adapterMock
2732+
}
2733+
lmsCreator.On("GetAdapter", "ISIL:REQ1").Return(lmsAdapter, nil)
2734+
mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr, nil)
2735+
prAction := CreatePatronRequestActionService(mockPrRepo, new(IllRepoMock), *new(events.EventBus), mockIso18626Handler, lmsCreator, new(EmailSenderMock))
2736+
action := BorrowerActionFillLocally
2737+
2738+
status, resultData := prAction.handleInvokeAction(appCtx, events.Event{
2739+
PatronRequestID: patronRequestId,
2740+
EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}},
2741+
})
2742+
2743+
assert.Equal(t, events.EventStatusSuccess, status)
2744+
if assert.NotNil(t, resultData.ActionResult) && assert.NotNil(t, resultData.ActionResult.ToState) {
2745+
assert.Equal(t, string(BorrowerStateCompleted), *resultData.ActionResult.ToState)
2746+
}
2747+
assert.Equal(t, BorrowerStateCompleted, mockPrRepo.savedPr.State)
2748+
assert.True(t, mockPrRepo.savedPr.TerminalState)
2749+
assert.False(t, mockPrRepo.savedPr.NeedsAttention)
2750+
if assert.NotNil(t, mockIso18626Handler.lastSupplyingAgencyMessage) {
2751+
assert.Equal(t, tt.expectedStatus, mockIso18626Handler.lastSupplyingAgencyMessage.StatusInfo.Status)
2752+
assert.Equal(t, iso18626.TypeReasonForMessageStatusChange, mockIso18626Handler.lastSupplyingAgencyMessage.MessageInfo.ReasonForMessage)
2753+
}
2754+
if adapterMock, ok := lmsAdapter.(*mockLmsAdapter); ok {
2755+
adapterMock.AssertExpectations(t)
2756+
}
2757+
})
2758+
}
2759+
}
2760+
26232761
type MockEventBus struct {
26242762
mock.Mock
26252763
events.EventBus
@@ -3088,6 +3226,16 @@ func TestLoadReturnableStateModel(t *testing.T) {
30883226
type mockLmsAdapter struct {
30893227
mock.Mock
30903228
lms.LmsAdapterManual
3229+
requesterPickupLocation string
3230+
itemLocation string
3231+
}
3232+
3233+
func (m *mockLmsAdapter) RequesterPickupLocation() string {
3234+
return m.requesterPickupLocation
3235+
}
3236+
3237+
func (m *mockLmsAdapter) ItemLocation() string {
3238+
return m.itemLocation
30913239
}
30923240

30933241
func (m *mockLmsAdapter) CancelRequestItem(requestId string, userId string) error {

broker/patron_request/service/message-handler.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessage(ctx common.Ex
181181
return m.handleSupplyingAgencyMessageWithParent(ctx, sam, pr, nil)
182182
}
183183

184+
func isLocalSupply(pr pr_db.PatronRequest, supplierSymbol string) bool {
185+
return pr.RequesterSymbol.Valid && pr.RequesterSymbol.String == supplierSymbol
186+
}
187+
184188
func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx common.ExtendedContext, sam iso18626.SupplyingAgencyMessage, pr pr_db.PatronRequest, parentEventID *string) (events.EventStatus, *iso18626.ISO18626Message, error) {
185189
unsupportedReason := func() (events.EventStatus, *iso18626.ISO18626Message, error) {
186190
err := fmt.Errorf("unsupported reason for message: %s", sam.MessageInfo.ReasonForMessage)
@@ -227,12 +231,14 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
227231
Valid: true,
228232
}
229233
}
230-
231234
eventName := MessageEvent("")
232235
var retryBibInfo *iso18626.BibliographicInfo
233236
switch sam.StatusInfo.Status {
234237
case iso18626.TypeStatusExpectToSupply:
235238
eventName = SupplierExpectToSupply
239+
if isLocalSupply(pr, supSymbol) {
240+
eventName = SupplierExpectToSupplyLocal
241+
}
236242
case iso18626.TypeStatusWillSupply:
237243
if sam.MessageInfo.ReasonForMessage == iso18626.TypeReasonForMessageCancelResponse {
238244
if sam.MessageInfo.AnswerYesNo != nil && *sam.MessageInfo.AnswerYesNo == iso18626.TypeYesNoY {
@@ -258,15 +264,23 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
258264
eventName = SupplierLoaned
259265
case iso18626.TypeStatusLoanCompleted, iso18626.TypeStatusCopyCompleted:
260266
eventName = SupplierCompleted
267+
if isLocalSupply(pr, supSymbol) {
268+
eventName = SupplierCompletedLocal
269+
}
261270
case iso18626.TypeStatusUnfilled:
262271
eventName = SupplierUnfilled
272+
if isLocalSupply(pr, supSymbol) {
273+
eventName = SupplierUnfilledLocal
274+
}
263275
case iso18626.TypeStatusCancelled:
264-
// Cancellation transition is accepted only for cancel-response messages.
265276
if sam.MessageInfo.ReasonForMessage == iso18626.TypeReasonForMessageCancelResponse {
266277
if sam.MessageInfo.AnswerYesNo != nil && *sam.MessageInfo.AnswerYesNo == iso18626.TypeYesNoN {
267278
return contradictoryCancelResponse()
268279
}
269280
eventName = SupplierCancelAccepted
281+
} else if sam.MessageInfo.ReasonForMessage == iso18626.TypeReasonForMessageStatusChange &&
282+
isLocalSupply(pr, supSymbol) {
283+
eventName = SupplierCancelledLocal
270284
}
271285
case iso18626.TypeStatusRetryPossible:
272286
eventName = SupplierRetryConditional
@@ -294,6 +308,15 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessageWithParent(ctx
294308
if !eventDefined {
295309
return statusChangeNotAllowed()
296310
}
311+
if stateChanged &&
312+
(eventName == SupplierCompletedLocal ||
313+
eventName == SupplierCancelledLocal ||
314+
eventName == SupplierUnfilledLocal) {
315+
ctx.Logger().Warn("ignoring transition configured for local audit event",
316+
"event", eventName, "state", pr.State)
317+
updatedPr = pr
318+
stateChanged = false
319+
}
297320
if retryBibInfo != nil {
298321
updatedPr.RetryBibInfo = retryBibInfo
299322
}

0 commit comments

Comments
 (0)