diff --git a/broker/migrations/020_add_attention_field.down.sql b/broker/migrations/020_add_attention_field.down.sql new file mode 100644 index 00000000..f52c09b2 --- /dev/null +++ b/broker/migrations/020_add_attention_field.down.sql @@ -0,0 +1,21 @@ +DROP VIEW patron_request_search_view ; + +ALTER TABLE patron_request DROP COLUMN needs_attention; + +DROP INDEX IF EXISTS idx_pr_state; +DROP INDEX IF EXISTS idx_pr_side; +DROP INDEX IF EXISTS idx_pr_requester_symbol; +DROP INDEX IF EXISTS idx_pr_supplier_symbol; +DROP INDEX IF EXISTS idx_pr_requester_req_id; +DROP INDEX IF EXISTS idx_pr_needs_attention; +DROP INDEX IF EXISTS idx_pr_timestamp; + +DROP INDEX IF EXISTS idx_pr_service_type; +DROP INDEX IF EXISTS idx_pr_service_level; +DROP INDEX IF EXISTS idx_pr_needed_at; + +DROP INDEX IF EXISTS idx_notification_pr_id; +DROP INDEX IF EXISTS idx_notification_pr_id_has_cost; +DROP INDEX IF EXISTS idx_notification_pr_id_unread; + +DROP FUNCTION IF EXISTS immutable_to_timestamp; \ No newline at end of file diff --git a/broker/migrations/020_add_attention_field.up.sql b/broker/migrations/020_add_attention_field.up.sql new file mode 100644 index 00000000..5ad15ec5 --- /dev/null +++ b/broker/migrations/020_add_attention_field.up.sql @@ -0,0 +1,50 @@ +ALTER TABLE patron_request ADD COLUMN needs_attention BOOLEAN NOT NULL DEFAULT false; + +CREATE OR REPLACE FUNCTION immutable_to_timestamp(text) +RETURNS timestamp +LANGUAGE sql +IMMUTABLE STRICT +AS $$ +SELECT $1::timestamp; +$$; + +CREATE OR REPLACE 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, + EXISTS ( + SELECT 1 + FROM notification n + WHERE n.pr_id = pr.id and acknowledged_at is null + ) 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 +FROM patron_request pr; + +CREATE INDEX idx_pr_state ON patron_request (state); +CREATE INDEX idx_pr_side ON patron_request (side); +CREATE INDEX idx_pr_requester_symbol ON patron_request (requester_symbol); +CREATE INDEX idx_pr_supplier_symbol ON patron_request (supplier_symbol); +CREATE INDEX idx_pr_requester_req_id ON patron_request (requester_req_id); +CREATE INDEX idx_pr_needs_attention ON patron_request (needs_attention); +CREATE INDEX idx_pr_timestamp ON patron_request (timestamp); + +CREATE INDEX idx_pr_service_type ON patron_request ((ill_request -> 'serviceInfo' ->> 'serviceType')); +CREATE INDEX idx_pr_service_level ON patron_request ((ill_request -> 'serviceInfo' -> 'serviceLevel' ->> '#text')); +CREATE INDEX idx_pr_needed_at ON patron_request (immutable_to_timestamp(ill_request -> 'serviceInfo' ->> 'needBeforeDate')); + +CREATE INDEX idx_notification_pr_id ON notification (pr_id); +CREATE INDEX idx_notification_pr_id_has_cost ON notification (pr_id) WHERE cost IS NOT NULL; +CREATE INDEX idx_notification_pr_id_unread ON notification (pr_id) WHERE acknowledged_at IS NULL; + + diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index 00da8b60..3ed0f534 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -465,6 +465,9 @@ components: requesterRequestId: type: string description: Requester patron request ID + needsAttention: + type: boolean + description: Indicates if the request needs attention lastAction: type: string description: Latest action on this request @@ -480,6 +483,7 @@ components: - state - side - illRequest + - needsAttention PatronRequests: type: object required: @@ -1121,6 +1125,11 @@ paths: /patron_requests: get: summary: Retrieve patron requests + description: | + Use this endpoint to retrieve patron requests. + Query parameter cql can be used to filter the results. + With cql you can use these fields state, side, requester_symbol, supplier_symbol, needs_attention, + has_notification, has_cost, has_unread_notification, service_type, service_level, created_at, needed_at, requester_req_id. tags: - patron-requests-api parameters: diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index e34589b2..efe0166d 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -536,6 +536,7 @@ func toApiPatronRequest(request pr_db.PatronRequest, illRequest iso18626.Request SupplierSymbol: toString(request.SupplierSymbol), IllRequest: utils.Must(common.StructToMap(illRequest)), RequesterRequestId: toString(request.RequesterReqID), + NeedsAttention: request.NeedsAttention, LastAction: toString(request.LastAction), LastActionOutcome: toString(request.LastActionOutcome), LastActionResult: toString(request.LastActionResult), diff --git a/broker/patron_request/db/prcql.go b/broker/patron_request/db/prcql.go index 3614dd9a..a7890407 100644 --- a/broker/patron_request/db/prcql.go +++ b/broker/patron_request/db/prcql.go @@ -40,6 +40,30 @@ func handlePatronRequestsQuery(cqlString string, noBaseArgs int) (pgcql.Query, e f = pgcql.NewFieldString().WithExact() def.AddField("requester_req_id", f) + f = pgcql.NewFieldString().WithExact() + def.AddField("needs_attention", f) + + fb := pgcql.NewFieldBool() + def.AddField("has_notification", fb) + + fb = pgcql.NewFieldBool() + def.AddField("has_cost", fb) + + fb = pgcql.NewFieldBool() + def.AddField("has_unread_notification", fb) + + f = pgcql.NewFieldString().WithExact() + def.AddField("service_type", f) + + f = pgcql.NewFieldString().WithExact() + def.AddField("service_level", f) + + nf := pgcql.NewFieldDate().WithColumn("timestamp") + def.AddField("created_at", nf) + + nf = pgcql.NewFieldDate() + def.AddField("needed_at", nf) + var parser cql.Parser query, err := parser.Parse(cqlString) if err != nil { @@ -53,7 +77,7 @@ func (q *Queries) ListPatronRequestsCql(ctx context.Context, db DBTX, arg ListPa if cqlString == nil { return q.ListPatronRequests(ctx, db, arg) } - noBaseArgs := 2 // weh have two base arguments: limit and offset + noBaseArgs := 2 // we have two base arguments: limit and offset res, err := handlePatronRequestsQuery(*cqlString, noBaseArgs) if err != nil { return nil, err @@ -80,19 +104,20 @@ func (q *Queries) ListPatronRequestsCql(ctx context.Context, db DBTX, arg ListPa for rows.Next() { var i ListPatronRequestsRow if err := rows.Scan( - &i.PatronRequest.ID, - &i.PatronRequest.Timestamp, - &i.PatronRequest.IllRequest, - &i.PatronRequest.State, - &i.PatronRequest.Side, - &i.PatronRequest.Patron, - &i.PatronRequest.RequesterSymbol, - &i.PatronRequest.SupplierSymbol, - &i.PatronRequest.Tenant, - &i.PatronRequest.RequesterReqID, - &i.PatronRequest.LastAction, - &i.PatronRequest.LastActionOutcome, - &i.PatronRequest.LastActionResult, + &i.ID, + &i.Timestamp, + &i.IllRequest, + &i.State, + &i.Side, + &i.Patron, + &i.RequesterSymbol, + &i.SupplierSymbol, + &i.Tenant, + &i.RequesterReqID, + &i.NeedsAttention, + &i.LastAction, + &i.LastActionOutcome, + &i.LastActionResult, &i.FullCount, ); err != nil { return nil, err diff --git a/broker/patron_request/db/prrepo.go b/broker/patron_request/db/prrepo.go index 94bd5b7b..563783ae 100644 --- a/broker/patron_request/db/prrepo.go +++ b/broker/patron_request/db/prrepo.go @@ -12,6 +12,7 @@ import ( type PrRepo interface { repo.Transactional[PrRepo] GetPatronRequestById(ctx common.ExtendedContext, id string) (PatronRequest, error) + GetPatronRequestByIdForUpdate(ctx common.ExtendedContext, id string) (PatronRequest, error) GetPatronRequestByIdAndSide(ctx common.ExtendedContext, id string, side PatronRequestSide) (PatronRequest, error) ListPatronRequests(ctx common.ExtendedContext, args ListPatronRequestsParams, cql *string) ([]PatronRequest, int64, error) UpdatePatronRequest(ctx common.ExtendedContext, params UpdatePatronRequestParams) (PatronRequest, error) @@ -49,6 +50,11 @@ func (r *PgPrRepo) GetPatronRequestById(ctx common.ExtendedContext, id string) ( return row.PatronRequest, err } +func (r *PgPrRepo) GetPatronRequestByIdForUpdate(ctx common.ExtendedContext, id string) (PatronRequest, error) { + row, err := r.queries.GetPatronRequestByIdForUpdate(ctx, r.GetConnOrTx(), id) + return row.PatronRequest, err +} + func (r *PgPrRepo) GetPatronRequestByIdAndSide(ctx common.ExtendedContext, id string, side PatronRequestSide) (PatronRequest, error) { pr, err := r.GetPatronRequestById(ctx, id) if err != nil { @@ -69,7 +75,22 @@ func (r *PgPrRepo) ListPatronRequests(ctx common.ExtendedContext, params ListPat fullCount = rows[0].FullCount for _, r := range rows { fullCount = r.FullCount - list = append(list, r.PatronRequest) + list = append(list, PatronRequest{ + ID: r.ID, + Timestamp: r.Timestamp, + IllRequest: r.IllRequest, + State: PatronRequestState(r.State), + Side: PatronRequestSide(r.Side), + Patron: r.Patron, + RequesterSymbol: r.RequesterSymbol, + SupplierSymbol: r.SupplierSymbol, + Tenant: r.Tenant, + RequesterReqID: r.RequesterReqID, + NeedsAttention: r.NeedsAttention, + LastAction: r.LastAction, + LastActionOutcome: r.LastActionOutcome, + LastActionResult: r.LastActionResult, + }) } } else { params.Limit = 1 diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index d39997b0..5bc07b75 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -97,6 +97,9 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended updatedPr.LastAction = getDbText(string(action)) updatedPr.LastActionOutcome = getDbText(execResult.outcome) updatedPr.LastActionResult = getDbText(string(execResult.status)) + if execResult.outcome == ActionOutcomeFailure { + updatedPr.NeedsAttention = true + } stateChanged := false if transitionState, ok := actionMapping.GetActionTransition(currentPr, action, execResult.outcome); ok && transitionState != updatedPr.State { updatedPr.State = transitionState @@ -112,6 +115,9 @@ func (a *PatronRequestActionService) finalizeActionExecution(ctx common.Extended if stateChanged { err := a.RunAutoActionsOnStateEntry(ctx, updatedPr, &event.ID) if err != nil { + if !updatedPr.NeedsAttention { + a.setNeedsAttention(ctx, updatedPr) + } return events.LogErrorAndReturnResult(ctx, "failed to execute auto action", err) } } @@ -721,6 +727,25 @@ func (a *PatronRequestActionService) checkSupplyingResponse(status events.EventS return actionExecutionResult{status: events.EventStatusSuccess, result: nil, outcome: ActionOutcomeSuccess, pr: pr} } +func (a *PatronRequestActionService) setNeedsAttention(ctx common.ExtendedContext, pr pr_db.PatronRequest) { + err := a.prRepo.WithTxFunc(ctx, func(repo pr_db.PrRepo) error { + prToUpdate, err := repo.GetPatronRequestByIdForUpdate(ctx, pr.ID) + if err != nil { + return err + } + if prToUpdate.NeedsAttention { + return nil + } + prToUpdate.NeedsAttention = true + _, err = repo.UpdatePatronRequest(ctx, pr_db.UpdatePatronRequestParams(prToUpdate)) + return err + }) + if err != nil { + ctx.Logger().Error("failed to set needs attention", "pr_id", pr.ID, "error", err) + return + } +} + type ResponseCaptureWriter struct { IllMessage *iso18626.ISO18626Message StatusCode int diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index 98c94c74..cbdd96e3 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -103,12 +103,14 @@ func TestHandleBorrowingActionMissingRequesterSymbol(t *testing.T) { lmsCreator.On("GetAdapter", "ISIL:x").Return(lms.CreateLmsAdapterMockOK(), nil) prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) illRequest := iso18626.Request{} - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "missing requester symbol", resultData.EventError.Message) + assert.True(t, mockPrRepo.savedPr.NeedsAttention) } func TestHandleInvokeActionValidateOK(t *testing.T) { @@ -136,7 +138,7 @@ func TestHandleInvokeActionValidateGetAdapterFailed(t *testing.T) { lmsCreator.On("GetAdapter", "ISIL:x").Return(lms.CreateLmsAdapterMockOK(), assert.AnError) prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) illRequest := iso18626.Request{} - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest, NeedsAttention: true}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -151,7 +153,8 @@ func TestHandleInvokeActionValidateLookupFailed(t *testing.T) { lmsCreator.On("GetAdapter", "ISIL:REC1").Return(createLmsAdapterMockFail(), nil) prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) illRequest := iso18626.Request{} - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -224,6 +227,7 @@ func TestHandleInvokeActionReceiveAcceptItemFailed(t *testing.T) { action := BorrowerActionReceive mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -240,6 +244,7 @@ func TestHandleInvokeActionReceiveNoItem(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionReceive status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -257,6 +262,7 @@ func TestHandleInvokeActionReceiveItemLookupFailure(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, assert.AnError) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionReceive status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -290,6 +296,7 @@ func TestHandleInvokeActionCheckOutItemFails(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, Patron: pgtype.Text{Valid: true, String: "patron1"}, State: BorrowerStateReceived, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, assert.AnError) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionCheckOut status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -306,6 +313,7 @@ func TestHandleInvokeActionCheckOutFails(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateReceived, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionCheckOut status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -338,6 +346,7 @@ func TestHandleInvokeActionCheckInItemFails(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateCheckedOut, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, assert.AnError) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionCheckIn status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -354,6 +363,7 @@ func TestHandleInvokeActionCheckInFails(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateCheckedOut, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionCheckIn status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -388,6 +398,7 @@ func TestHandleInvokeActionShipReturnItemFails(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, assert.AnError) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionShipReturn status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -405,6 +416,7 @@ func TestHandleInvokeActionShipReturnFails(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := BorrowerActionShipReturn status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -579,7 +591,8 @@ func TestHandleInvokeLenderActionNoSupplierSymbol(t *testing.T) { lmsCreator := new(MockLmsCreator) prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) illRequest := iso18626.Request{} - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: LenderStateNew, Side: SideLending}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateNew, Side: SideLending}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -594,7 +607,8 @@ func TestHandleInvokeLenderActionNoLms(t *testing.T) { prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) illRequest := iso18626.Request{} - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: LenderStateNew, Side: SideLending, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateNew, Side: SideLending, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -644,6 +658,43 @@ func TestHandleInvokeLenderActionValidate(t *testing.T) { assert.Equal(t, LenderActionWillSupply, *mockEventBus.createdTaskData[0].Action) } +func TestHandleInvokeLenderActionValidateAutoActionError(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockEventBus.runTaskHandler = true + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", "ISIL:SUP1").Return(createLmsAdapterMockLog(), nil) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, mockEventBus, mockIso18626Handler, lmsCreator) + illRequest := iso18626.Request{} + + initialPR := pr_db.PatronRequest{ + ID: patronRequestId, + IllRequest: illRequest, + State: LenderStateNew, + Side: SideLending, + SupplierSymbol: getDbText("ISIL:SUP1"), + RequesterSymbol: getDbText("ISIL:REQ1"), + } + validatedPR := initialPR + validatedPR.State = LenderStateValidated + + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(initialPR, nil).Once() + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(validatedPR, errors.New("db error")).Once() + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(validatedPR, nil).Once() + mockEventBus.On("CreateNoticeWithParent", "invoke-validate").Return("", nil) + + status, _ := prAction.handleInvokeAction(appCtx, events.Event{ + ID: "invoke-validate", + PatronRequestID: patronRequestId, + EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}, + }) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, LenderStateValidated, mockPrRepo.savedPr.State) + assert.True(t, mockPrRepo.savedPr.NeedsAttention) +} + func TestHandleInvokeLenderActionWillSupplyUseIllTitleWhenRequestItemEmptyOK(t *testing.T) { mockPrRepo := new(MockPrRepo) lmsCreator := new(MockLmsCreator) @@ -730,7 +781,8 @@ func TestHandleInvokeLenderActionWillSupplyNcipFailed(t *testing.T) { 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) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionWillSupply status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -745,8 +797,9 @@ func TestHandleInvokeLenderActionWillSupplySaveItemFailed(t *testing.T) { 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) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.saveItemFail = true + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionWillSupply status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -873,6 +926,7 @@ func TestHandleInvokeLenderActionShipNewTitleFail(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateWillSupply, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) mockPrRepo.saveItemFail = true action := LenderActionShip @@ -894,6 +948,7 @@ func TestHandleInvokeLenderActionShipGetItemsByIdFail(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateWillSupply, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, assert.AnError) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionShip @@ -913,6 +968,7 @@ func TestHandleInvokeLenderActionShipGetItemsByIdEmpty(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateWillSupply, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionShip @@ -933,6 +989,7 @@ func TestHandleInvokeLenderActionShipLmsFailed(t *testing.T) { mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateWillSupply, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) action := LenderActionShip + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -967,6 +1024,7 @@ func TestHandleInvokeLenderActionMarkReceivedNoItems(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateShippedReturn, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionMarkReceived status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -983,6 +1041,7 @@ func TestHandleInvokeLenderActionMarkReceivedLmsFailed(t *testing.T) { illRequest := iso18626.Request{} mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateShippedReturn, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) mockPrRepo.On("GetItemsByPrId", patronRequestId).Return([]pr_db.Item{{Barcode: "1234"}}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionMarkReceived status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -1020,7 +1079,8 @@ func TestHandleInvokeLenderActionAcceptCancelMissingRequesterSymbol(t *testing.T 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: LenderStateCancelRequested, Side: SideLending, RequesterSymbol: pgtype.Text{Valid: false, String: ""}, SupplierSymbol: getDbText("ISIL:SUP1")}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{ID: patronRequestId, IllRequest: illRequest, State: LenderStateCancelRequested, Side: SideLending, RequesterSymbol: pgtype.Text{Valid: false, String: ""}, SupplierSymbol: getDbText("ISIL:SUP1")}, nil) + mockPrRepo.On("GetPatronRequestByIdForUpdate", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}, IllRequest: illRequest}, nil) action := LenderActionAcceptCancel status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -1081,11 +1141,20 @@ type MockPrRepo struct { saveItemFail bool } +func (r *MockPrRepo) WithTxFunc(ctx common.ExtendedContext, fn func(repo pr_db.PrRepo) error) error { + return fn(r) +} + func (r *MockPrRepo) GetPatronRequestById(ctx common.ExtendedContext, id string) (pr_db.PatronRequest, error) { args := r.Called(id) return args.Get(0).(pr_db.PatronRequest), args.Error(1) } +func (r *MockPrRepo) GetPatronRequestByIdForUpdate(ctx common.ExtendedContext, id string) (pr_db.PatronRequest, error) { + args := r.Called(id) + return args.Get(0).(pr_db.PatronRequest), args.Error(1) +} + func (r *MockPrRepo) GetPatronRequestByIdAndSide(ctx common.ExtendedContext, id string, side pr_db.PatronRequestSide) (pr_db.PatronRequest, error) { args := r.Called(id, side) return args.Get(0).(pr_db.PatronRequest), args.Error(1) diff --git a/broker/sqlc/pr_query.sql b/broker/sqlc/pr_query.sql index 28ce3e26..e0918204 100644 --- a/broker/sqlc/pr_query.sql +++ b/broker/sqlc/pr_query.sql @@ -4,9 +4,16 @@ FROM patron_request WHERE id = $1 LIMIT 1; --- name: ListPatronRequests :many -SELECT sqlc.embed(patron_request), COUNT(*) OVER () as full_count +-- name: GetPatronRequestByIdForUpdate :one +SELECT sqlc.embed(patron_request) FROM patron_request +WHERE id = $1 + FOR UPDATE + LIMIT 1; + +-- name: ListPatronRequests :many +SELECT id, timestamp, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result, COUNT(*) OVER () as full_count +FROM patron_request_search_view ORDER BY timestamp LIMIT $1 OFFSET $2; @@ -21,15 +28,16 @@ SET timestamp = $2, supplier_symbol = $8, tenant = $9, requester_req_id = $10, - last_action = $11, - last_action_outcome = $12, - last_action_result = $13 + needs_attention = $11, + last_action = $12, + last_action_outcome = $13, + last_action_result = $14 WHERE id = $1 RETURNING sqlc.embed(patron_request); -- name: CreatePatronRequest :one -INSERT INTO patron_request (id, timestamp, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, last_action, last_action_outcome, last_action_result) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) +INSERT INTO patron_request (id, timestamp, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id, needs_attention, last_action, last_action_outcome, last_action_result) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING sqlc.embed(patron_request); -- name: DeletePatronRequest :exec diff --git a/broker/sqlc/pr_schema.sql b/broker/sqlc/pr_schema.sql index f04a6600..6a89b85f 100644 --- a/broker/sqlc/pr_schema.sql +++ b/broker/sqlc/pr_schema.sql @@ -11,6 +11,7 @@ CREATE TABLE patron_request supplier_symbol VARCHAR, tenant VARCHAR, requester_req_id VARCHAR, + needs_attention BOOLEAN NOT NULL DEFAULT false, last_action VARCHAR, last_action_outcome VARCHAR, last_action_result VARCHAR @@ -49,3 +50,34 @@ CREATE TABLE notification created_at TIMESTAMP NOT NULL DEFAULT now(), acknowledged_at TIMESTAMP ); + +CREATE OR REPLACE FUNCTION immutable_to_timestamp(text) +RETURNS timestamp +LANGUAGE sql +IMMUTABLE STRICT +AS $$ +SELECT $1::timestamp; +$$; + +CREATE OR REPLACE 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, + EXISTS ( + SELECT 1 + FROM notification n + WHERE n.pr_id = pr.id and acknowledged_at is null + ) 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 +FROM patron_request pr; \ No newline at end of file diff --git a/broker/sqlc/sqlc.yaml b/broker/sqlc/sqlc.yaml index f02b46e9..5a774b7a 100644 --- a/broker/sqlc/sqlc.yaml +++ b/broker/sqlc/sqlc.yaml @@ -74,6 +74,10 @@ sql: go_type: import: "github.com/indexdata/crosslink/iso18626" type: "Request" + - column: "patron_request_search_view.ill_request" + go_type: + import: "github.com/indexdata/crosslink/iso18626" + type: "Request" - column: "patron_request.state" go_type: type: "PatronRequestState" diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index e026a53f..2ad6bb73 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -103,6 +103,15 @@ func TestCrud(t *testing.T) { SupplierUniqueRecordId: "WILLSUPPLY_LOANED", Title: "Typed request round trip", }, + ServiceInfo: &iso18626.ServiceInfo{ + ServiceLevel: &iso18626.TypeSchemeValuePair{ + Text: "Copy", + }, + ServiceType: iso18626.TypeServiceTypeCopy, + NeedBeforeDate: &utils.XSDDateTime{ + Time: time.Now().Add(24 * time.Hour), + }, + }, } id := uuid.NewString() newPr := proapi.CreatePatronRequest{ @@ -140,6 +149,8 @@ func TestCrud(t *testing.T) { assert.Equal(t, "success", *foundPr.LastActionOutcome) assert.Equal(t, "SUCCESS", *foundPr.LastActionResult) + assert.Equal(t, false, foundPr.NeedsAttention) + respBytes = httpRequest(t, "POST", basePath, newPrBytes, 400) assert.Contains(t, string(respBytes), "a patron request with this ID already exists") @@ -167,6 +178,18 @@ func TestCrud(t *testing.T) { assert.Equal(t, int64(1), foundPrs.About.Count) assert.Len(t, foundPrs.Items, 0) + // GET list with all query params + respBytes = httpRequest(t, "GET", basePath+queryParams+"&cql=state%3DVALIDATED%20and%20"+ + "side%3Dborrowing%20and%20requester_symbol%3D"+*foundPr.RequesterSymbol+ + "%20and%20requester_req_id%3D"+*foundPr.RequesterRequestId+"%20and%20needs_attention%3Dfalse%20and%20"+ + "has_notification%3Dfalse%20and%20has_cost%3Dfalse%20and%20has_unread_notification%3Dfalse%20and%20"+ + "service_type%3DCopy%20and%20service_level%3DCopy%20and%20created_at%3E2026-03-16%20and%20needed_at%3E2026-03-16", []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPrs) + assert.NoError(t, err, "failed to unmarshal patron request") + + assert.Equal(t, int64(1), foundPrs.About.Count) + assert.Len(t, foundPrs.Items, 1) + // GET by id with symbol and side thisPrPath := basePath + "/" + *newPr.Id respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200)