diff --git a/broker/common/common.go b/broker/common/common.go index 9106e210..0ae7fdcf 100644 --- a/broker/common/common.go +++ b/broker/common/common.go @@ -4,8 +4,13 @@ import ( "fmt" "reflect" "strings" + + "github.com/indexdata/crosslink/iso18626" ) +const MULTIPLE_ITEMS = "#MultipleItems#" +const MULTIPLE_ITEMS_END = "#MultipleItemsEnd#" + func StructToMap(obj interface{}) (map[string]interface{}, error) { result := make(map[string]interface{}) val := reflect.ValueOf(obj) @@ -37,3 +42,63 @@ func StructToMap(obj interface{}) (map[string]interface{}, error) { return result, nil } + +func SamHasItems(sam iso18626.SupplyingAgencyMessage) bool { + return strings.Contains(sam.MessageInfo.Note, MULTIPLE_ITEMS) && strings.Contains(sam.MessageInfo.Note, MULTIPLE_ITEMS_END) +} + +func GetItemParams(note string) ([][]string, int, int) { + startIdx := strings.Index(note, MULTIPLE_ITEMS) + endIdx := strings.Index(note, MULTIPLE_ITEMS_END) + + // Validate indices to avoid panics if markers are missing or misordered. + if startIdx < 0 || endIdx < 0 || endIdx <= startIdx { + return nil, startIdx, endIdx + } + + content := note[startIdx+len(MULTIPLE_ITEMS) : endIdx] + content = strings.TrimSpace(content) + var result [][]string + for _, f := range strings.Split(content, "\n") { + result = append(result, UnpackItemsNote(f)) + } + return result, startIdx, endIdx +} + +func PackItemsNote(fields []string) string { + escaped := make([]string, len(fields)) + for i, f := range fields { + // Escape backslashes first, then the separator + temp := strings.ReplaceAll(f, "\\", "\\\\") + escaped[i] = strings.ReplaceAll(temp, "|", "\\|") + } + return strings.Join(escaped, "|") +} + +func UnpackItemsNote(input string) []string { + var result []string + var current strings.Builder + escaped := false + + for i := 0; i < len(input); i++ { + char := input[i] + + if escaped { + current.WriteByte(char) + escaped = false + continue + } + + switch char { + case '\\': + escaped = true + case '|': + result = append(result, current.String()) + current.Reset() + default: + current.WriteByte(char) + } + } + result = append(result, current.String()) + return result +} diff --git a/broker/common/common_test.go b/broker/common/common_test.go index 5c14bca9..db2f8767 100644 --- a/broker/common/common_test.go +++ b/broker/common/common_test.go @@ -3,6 +3,8 @@ package common import ( "reflect" "testing" + + "github.com/stretchr/testify/assert" ) type User struct { @@ -68,3 +70,26 @@ func TestStructToMap(t *testing.T) { }) } } + +func TestGetItemParams(t *testing.T) { + // Just ID + note := MULTIPLE_ITEMS + "\n1\n" + MULTIPLE_ITEMS_END + result, startIdx, endIdx := GetItemParams(note) + assert.Equal(t, 0, startIdx) + assert.Equal(t, 18, endIdx) + assert.Equal(t, [][]string{{"1"}}, result) + + // All params + note = MULTIPLE_ITEMS + "\n1|2\\||3\\\\\n" + MULTIPLE_ITEMS_END + result, startIdx, endIdx = GetItemParams(note) + assert.Equal(t, 0, startIdx) + assert.Equal(t, 26, endIdx) + assert.Equal(t, [][]string{{"1", "2|", "3\\"}}, result) + + // Incorrect tag order + note = MULTIPLE_ITEMS_END + "\n1\n" + MULTIPLE_ITEMS + result, startIdx, endIdx = GetItemParams(note) + assert.Equal(t, 21, startIdx) + assert.Equal(t, 0, endIdx) + assert.Nil(t, result) +} diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index 223dece8..94780d78 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -709,6 +709,35 @@ components: required: - name + PrItem: + type: object + title: Item + description: Patron request item + properties: + id: + type: string + description: Item system id + barcode: + type: string + description: Item barcode + title: + type: string + description: Item title + callNumber: + type: string + description: Item call number + itemId: + type: string + description: Item item id + createdAt: + type: string + format: date-time + description: Item creation date time + required: + - id + - barcode + - createdAt + paths: /: get: @@ -1276,6 +1305,49 @@ paths: schema: $ref: '#/components/schemas/Error' + /patron_requests/{id}/items: + get: + summary: Retrieve patron request related items + parameters: + - in: path + name: id + schema: + type: string + required: true + description: ID of the patron request + - $ref: '#/components/parameters/Tenant' + - $ref: '#/components/parameters/Side' + - $ref: '#/components/parameters/Symbol' + tags: + - patron-requests-api + responses: + '200': + description: Successful retrieval of patron request items + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PrItem' + '400': + description: Bad Request. Invalid query parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found. Patron request not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /sse/events: get: summary: Subscribe to real-time notifications. Notification is send out every time when ISO18626 message is sent out diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index 83aba421..eda2fb43 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -247,13 +247,8 @@ func (a *PatronRequestApiHandler) DeletePatronRequestsId(w http.ResponseWriter, addBadRequestError(ctx, w, err) return } - pr, err := a.getPatronRequestById(ctx, id, params.Side, symbol) - if err != nil { - addInternalError(ctx, w, err) - return - } + pr := a.getPatronRequestById(w, ctx, id, params.Side, symbol) if pr == nil { - addNotFoundError(w) return } err = a.prRepo.WithTxFunc(ctx, func(repo pr_db.PrRepo) error { @@ -266,18 +261,21 @@ func (a *PatronRequestApiHandler) DeletePatronRequestsId(w http.ResponseWriter, w.WriteHeader(http.StatusNoContent) } -func (a *PatronRequestApiHandler) getPatronRequestById(ctx common.ExtendedContext, id string, side *string, symbol string) (*pr_db.PatronRequest, error) { +func (a *PatronRequestApiHandler) getPatronRequestById(w http.ResponseWriter, ctx common.ExtendedContext, id string, side *string, symbol string) *pr_db.PatronRequest { pr, err := a.prRepo.GetPatronRequestById(ctx, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return nil, nil + addNotFoundError(w) + return nil } - return nil, err + addInternalError(ctx, w, err) + return nil } if isOwner(pr, symbol) && (!isSideParamValid(side) || string(pr.Side) == *side) { - return &pr, nil + return &pr } - return nil, nil + addNotFoundError(w) + return nil } func isSideParamValid(side *string) bool { @@ -301,13 +299,8 @@ func (a *PatronRequestApiHandler) GetPatronRequestsId(w http.ResponseWriter, r * addBadRequestError(ctx, w, err) return } - pr, err := a.getPatronRequestById(ctx, id, params.Side, symbol) - if err != nil { - addInternalError(ctx, w, err) - return - } + pr := a.getPatronRequestById(w, ctx, id, params.Side, symbol) if pr == nil { - addNotFoundError(w) return } var illRequest iso18626.Request @@ -331,13 +324,8 @@ func (a *PatronRequestApiHandler) GetPatronRequestsIdActions(w http.ResponseWrit addBadRequestError(ctx, w, err) return } - pr, err := a.getPatronRequestById(ctx, id, params.Side, symbol) - if err != nil { - addInternalError(ctx, w, err) - return - } + pr := a.getPatronRequestById(w, ctx, id, params.Side, symbol) if pr == nil { - addNotFoundError(w) return } actionMapping, err := a.actionMappingService.GetActionMapping(*pr) @@ -361,13 +349,8 @@ func (a *PatronRequestApiHandler) PostPatronRequestsIdAction(w http.ResponseWrit addBadRequestError(ctx, w, err) return } - pr, err := a.getPatronRequestById(ctx, id, params.Side, symbol) - if err != nil { - addInternalError(ctx, w, err) - return - } + pr := a.getPatronRequestById(w, ctx, id, params.Side, symbol) if pr == nil { - addNotFoundError(w) return } var action proapi.ExecuteAction @@ -422,6 +405,7 @@ func (a *PatronRequestApiHandler) PostPatronRequestsIdAction(w http.ResponseWrit func (a *PatronRequestApiHandler) GetPatronRequestsIdEvents(w http.ResponseWriter, r *http.Request, id string, params proapi.GetPatronRequestsIdEventsParams) { symbol, err := api.GetSymbolForRequest(r, a.tenant, params.XOkapiTenant, params.Symbol) logParams := map[string]string{"method": "GetPatronRequestsIdEvents", "id": id, "symbol": symbol} + if params.Side != nil { logParams["side"] = *params.Side } @@ -431,24 +415,49 @@ func (a *PatronRequestApiHandler) GetPatronRequestsIdEvents(w http.ResponseWrite addBadRequestError(ctx, w, err) return } - pr, err := a.getPatronRequestById(ctx, id, params.Side, symbol) + pr := a.getPatronRequestById(w, ctx, id, params.Side, symbol) + if pr == nil { + return + } + eventsList, err := a.eventRepo.GetPatronRequestEvents(ctx, pr.ID) if err != nil { addInternalError(ctx, w, err) return } + + var responseItems []oapi.Event + for _, event := range eventsList { + responseItems = append(responseItems, api.ToApiEvent(event, "", &event.PatronRequestID)) + } + writeJsonResponse(w, responseItems) +} + +func (a *PatronRequestApiHandler) GetPatronRequestsIdItems(w http.ResponseWriter, r *http.Request, id string, params proapi.GetPatronRequestsIdItemsParams) { + symbol, err := api.GetSymbolForRequest(r, a.tenant, params.XOkapiTenant, params.Symbol) + logParams := map[string]string{"method": "GetPatronRequestsIdItems", "id": id, "symbol": symbol} + + if params.Side != nil { + logParams["side"] = *params.Side + } + ctx := common.CreateExtCtxWithArgs(context.Background(), &common.LoggerArgs{Other: logParams}) + + if err != nil { + addBadRequestError(ctx, w, err) + return + } + pr := a.getPatronRequestById(w, ctx, id, params.Side, symbol) if pr == nil { - addNotFoundError(w) return } - events, err := a.eventRepo.GetPatronRequestEvents(ctx, pr.ID) + itemsList, err := a.prRepo.GetItemsByPrId(ctx, pr.ID) if err != nil { addInternalError(ctx, w, err) return } - var responseItems []oapi.Event - for _, event := range events { - responseItems = append(responseItems, api.ToApiEvent(event, "", &event.PatronRequestID)) + var responseItems []proapi.PrItem + for _, item := range itemsList { + responseItems = append(responseItems, toApiItem(item)) } writeJsonResponse(w, responseItems) } @@ -568,3 +577,14 @@ func getDbText(value *string) pgtype.Text { String: *value, } } + +func toApiItem(item pr_db.Item) proapi.PrItem { + return proapi.PrItem{ + Id: item.ID, + Barcode: item.Barcode, + CallNumber: toString(item.CallNumber), + ItemId: toString(item.ItemID), + Title: toString(item.Title), + CreatedAt: item.CreatedAt.Time, + } +} diff --git a/broker/patron_request/db/prrepo.go b/broker/patron_request/db/prrepo.go index b8c8fe45..c59220e0 100644 --- a/broker/patron_request/db/prrepo.go +++ b/broker/patron_request/db/prrepo.go @@ -19,7 +19,7 @@ type PrRepo interface { GetNextHrid(ctx common.ExtendedContext, prefix string) (string, error) SaveItem(ctx common.ExtendedContext, params SaveItemParams) (Item, error) GetItemById(ctx common.ExtendedContext, id string) (Item, error) - GetItemByPrId(ctx common.ExtendedContext, prId string) ([]Item, error) + GetItemsByPrId(ctx common.ExtendedContext, prId string) ([]Item, error) SaveNotification(ctx common.ExtendedContext, params SaveNotificationParams) (Notification, error) GetNotificationById(ctx common.ExtendedContext, id string) (Notification, error) GetNotificationsByPrId(ctx common.ExtendedContext, prId string) ([]Notification, error) @@ -111,7 +111,7 @@ func (r *PgPrRepo) GetItemById(ctx common.ExtendedContext, id string) (Item, err return row.Item, err } -func (r *PgPrRepo) GetItemByPrId(ctx common.ExtendedContext, prId string) ([]Item, error) { +func (r *PgPrRepo) GetItemsByPrId(ctx common.ExtendedContext, prId string) ([]Item, error) { rows, err := r.queries.GetItemsByPrId(ctx, r.GetConnOrTx(), prId) var list []Item for _, row := range rows { diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index 96fe5811..05a659c7 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -705,7 +705,8 @@ func (m *MockEventBus) CreateNotice(id string, eventName events.EventName, data type MockPrRepo struct { mock.Mock pr_db.PgPrRepo - savedPr pr_db.PatronRequest + savedPr pr_db.PatronRequest + savedItems []pr_db.Item } func (r *MockPrRepo) GetPatronRequestById(ctx common.ExtendedContext, id string) (pr_db.PatronRequest, error) { @@ -734,6 +735,14 @@ func (r *MockPrRepo) GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx commo return args.Get(0).(pr_db.PatronRequest), args.Error(1) } +func (r *MockPrRepo) SaveItem(ctx common.ExtendedContext, params pr_db.SaveItemParams) (pr_db.Item, error) { + if r.savedItems == nil { + r.savedItems = []pr_db.Item{} + } + r.savedItems = append(r.savedItems, pr_db.Item(params)) + return pr_db.Item(params), nil +} + type MockIso18626Handler struct { mock.Mock handler.Iso18626Handler diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index d918bd1c..ccbf9fbe 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "time" @@ -176,6 +177,13 @@ func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessage(ctx common.Ex eventName = SupplierWillSupply } case iso18626.TypeStatusLoaned: + err := m.saveItems(ctx, pr, sam) + if err != nil { + return createSAMResponse(sam, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) + } eventName = SupplierLoaned case iso18626.TypeStatusLoanCompleted, iso18626.TypeStatusCopyCompleted: eventName = SupplierCompleted @@ -409,3 +417,40 @@ func (m *PatronRequestMessageHandler) updatePatronRequestAndCreateRamResponse(ct } return createRAMResponse(ram, iso18626.TypeMessageStatusOK, action, nil, nil) } + +func (m *PatronRequestMessageHandler) saveItems(ctx common.ExtendedContext, pr pr_db.PatronRequest, sam iso18626.SupplyingAgencyMessage) error { + if common.SamHasItems(sam) { + result, _, _ := common.GetItemParams(sam.MessageInfo.Note) + for _, item := range result { + var loopErr error + if len(item) == 1 && item[0] != "" { + loopErr = m.saveItem(ctx, pr.ID, item[0], item[0], nil) + } else if len(item) == 3 { + loopErr = m.saveItem(ctx, pr.ID, item[2], item[0], &item[1]) + } else { + loopErr = errors.New("incorrect item param count: " + strconv.Itoa(len(item))) + } + if loopErr != nil { + return loopErr + } + } + } + return nil +} + +func (m *PatronRequestMessageHandler) saveItem(ctx common.ExtendedContext, prId string, id string, name string, callNumber *string) error { + dbCallNumber := pgtype.Text{Valid: false, String: ""} + if callNumber != nil { + dbCallNumber = pgtype.Text{Valid: true, String: *callNumber} + } + _, err := m.prRepo.SaveItem(ctx, pr_db.SaveItemParams{ + ID: uuid.NewString(), + CreatedAt: pgtype.Timestamp{Valid: true, Time: time.Now()}, + PrID: prId, + ItemID: getDbText(id), + Title: getDbText(name), + CallNumber: dbCallNumber, + Barcode: id, //TODO barcode generation. How to do that? + }) + return err +} diff --git a/broker/patron_request/service/message-handler_test.go b/broker/patron_request/service/message-handler_test.go index 2964fdbe..85150138 100644 --- a/broker/patron_request/service/message-handler_test.go +++ b/broker/patron_request/service/message-handler_test.go @@ -288,15 +288,17 @@ func TestHandleSupplyingAgencyMessageLoaned(t *testing.T) { Header: iso18626.Header{ RequestingAgencyRequestId: patronRequestId, }, + StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusLoaned}, MessageInfo: iso18626.MessageInfo{ ReasonForMessage: iso18626.TypeReasonForMessageStatusChange, + Note: "#MultipleItems#\n1|2|3\n#MultipleItemsEnd#", }, - StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusLoaned}, }, pr_db.PatronRequest{State: BorrowerStateWillSupply, Side: SideBorrowing}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateShipped, mockPrRepo.savedPr.State) assert.NoError(t, err) + assert.Len(t, mockPrRepo.savedItems, 1) } func TestHandleSupplyingAgencyMessageLoanCompleted(t *testing.T) { @@ -749,3 +751,43 @@ func TestHandleRequestMessageSaveError(t *testing.T) { assert.Equal(t, "db error", resp.RequestConfirmation.ErrorData.ErrorValue) assert.Equal(t, "db error", err.Error()) } + +func TestSaveItems(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), mockEventBus) + + // Empty message + sam := iso18626.SupplyingAgencyMessage{} + err := handler.saveItems(appCtx, pr_db.PatronRequest{ID: "pr1"}, sam) + assert.NoError(t, err) + assert.Equal(t, 0, len(mockPrRepo.savedItems)) + + // One Item + sam.MessageInfo.Note = "#MultipleItems#\n1|2|3\n#MultipleItemsEnd#" + err = handler.saveItems(appCtx, pr_db.PatronRequest{ID: "pr1"}, sam) + assert.NoError(t, err) + assert.Equal(t, 1, len(mockPrRepo.savedItems)) + assert.Equal(t, "1", mockPrRepo.savedItems[0].Title.String) + assert.Equal(t, "2", mockPrRepo.savedItems[0].CallNumber.String) + assert.Equal(t, "3", mockPrRepo.savedItems[0].ItemID.String) + assert.Equal(t, "3", mockPrRepo.savedItems[0].Barcode) + assert.Equal(t, "pr1", mockPrRepo.savedItems[0].PrID) + + // Two Items + sam.MessageInfo.Note = "#MultipleItems#\n1|2|3\n4,5|6|7\n#MultipleItemsEnd#" + mockPrRepo.savedItems = nil + err = handler.saveItems(appCtx, pr_db.PatronRequest{ID: "pr1"}, sam) + assert.NoError(t, err) + assert.Equal(t, 2, len(mockPrRepo.savedItems)) + assert.Equal(t, "1", mockPrRepo.savedItems[0].Title.String) + assert.Equal(t, "2", mockPrRepo.savedItems[0].CallNumber.String) + assert.Equal(t, "3", mockPrRepo.savedItems[0].ItemID.String) + assert.Equal(t, "3", mockPrRepo.savedItems[0].Barcode) + assert.Equal(t, "pr1", mockPrRepo.savedItems[0].PrID) + assert.Equal(t, "4,5", mockPrRepo.savedItems[1].Title.String) + assert.Equal(t, "6", mockPrRepo.savedItems[1].CallNumber.String) + assert.Equal(t, "7", mockPrRepo.savedItems[1].ItemID.String) + assert.Equal(t, "7", mockPrRepo.savedItems[1].Barcode) + assert.Equal(t, "pr1", mockPrRepo.savedItems[1].PrID) +} diff --git a/broker/shim/shim.go b/broker/shim/shim.go index 32dae708..47a4fad7 100644 --- a/broker/shim/shim.go +++ b/broker/shim/shim.go @@ -5,6 +5,7 @@ import ( "regexp" "strings" + "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/ill_db" "github.com/indexdata/crosslink/directory" "github.com/indexdata/crosslink/iso18626" @@ -33,6 +34,7 @@ const LOAN_CONDITION_OTHER = "other" //non-standard LC used by ReShare var rsNoteRegexp = regexp.MustCompile(`#seq:[0-9]+#`) var edgeNonWord = regexp.MustCompile(`^\W+|\W+$`) +var reshareItemRegex = regexp.MustCompile(`(.*),(.*),(.*)`) type Iso18626Shim interface { ApplyToOutgoingRequest(message *iso18626.ISO18626Message) ([]byte, error) @@ -88,6 +90,11 @@ func (i *Iso18626AlmaShim) ApplyToIncomingRequest(message *iso18626.ISO18626Mess copyMessage.RequestingAgencyMessage.Header.SupplyingAgencyId.AgencyIdValue = symbol[1] } } + if message.SupplyingAgencyMessage != nil { + copySam := *message.SupplyingAgencyMessage + copyMessage.SupplyingAgencyMessage = ©Sam + i.unifyItem(copyMessage.SupplyingAgencyMessage) + } return ©Message } @@ -107,6 +114,7 @@ func (i *Iso18626AlmaShim) ApplyToOutgoingRequest(message *iso18626.ISO18626Mess i.appendReturnAddressToSuppMsgNote(suppMsg) } i.appendUnfilledStatusAndReasonUnfilled(suppMsg) + i.setItemId(suppMsg) } if message.Request != nil { request := message.Request @@ -198,6 +206,21 @@ func (i *Iso18626AlmaShim) appendUnfilledStatusAndReasonUnfilled(suppMsg *iso186 } } +func (i *Iso18626AlmaShim) setItemId(sam *iso18626.SupplyingAgencyMessage) { + if common.SamHasItems(*sam) { + result, startIdx, endIdx := common.GetItemParams(sam.MessageInfo.Note) + var items []string + for _, item := range result { + items = append(items, item[0]) + } + sam.DeliveryInfo.ItemId = strings.Join(items, ",") + + tillIndex := max(0, startIdx-1) // -1 because we remove new line symbol but index cannot be negative + sam.MessageInfo.Note = sam.MessageInfo.Note[0:tillIndex] + + sam.MessageInfo.Note[endIdx+len(common.MULTIPLE_ITEMS_END):] + } +} + func (i *Iso18626AlmaShim) transferOfferedCostsToDeliveryCosts(suppMsg *iso18626.SupplyingAgencyMessage) { //alma doesn't care about the delivery costs unless the status is Loaned or CopyCompleted if suppMsg.StatusInfo.Status != iso18626.TypeStatusLoaned && suppMsg.StatusInfo.Status != iso18626.TypeStatusCopyCompleted { @@ -516,6 +539,26 @@ func (i *Iso18626AlmaShim) fixRequesterConditionNote(requestingAgencyMessage *is } } +func (i *Iso18626AlmaShim) unifyItem(sam *iso18626.SupplyingAgencyMessage) { + if sam.DeliveryInfo != nil && sam.DeliveryInfo.ItemId != "" { + var sb strings.Builder + //retain original note + if sam.MessageInfo.Note != "" { + sb.WriteString(sam.MessageInfo.Note) + sb.WriteString("\n") + } + sb.WriteString(common.MULTIPLE_ITEMS) + sb.WriteString("\n") + list := strings.Split(sam.DeliveryInfo.ItemId, ",") + for _, item := range list { + sb.WriteString(common.PackItemsNote([]string{item})) + sb.WriteString("\n") + } + sb.WriteString(common.MULTIPLE_ITEMS_END) + sam.MessageInfo.Note = sb.String() + } +} + type Iso18626ReShareShim struct { Iso18626DefaultShim } @@ -523,6 +566,7 @@ type Iso18626ReShareShim struct { func (i *Iso18626ReShareShim) ApplyToOutgoingRequest(message *iso18626.ISO18626Message) ([]byte, error) { if message.SupplyingAgencyMessage != nil { i.transferDeliveryCostsToOfferedCosts(message.SupplyingAgencyMessage) + i.setItemId(message.SupplyingAgencyMessage) } return xml.Marshal(message) } @@ -543,3 +587,71 @@ func (i *Iso18626ReShareShim) transferDeliveryCostsToOfferedCosts(suppMsg *iso18 } } } + +func (i *Iso18626ReShareShim) ApplyToIncomingRequest(message *iso18626.ISO18626Message, requester *ill_db.Peer, supplier *ill_db.LocatedSupplier) *iso18626.ISO18626Message { + if message == nil { + return message + } + copyMessage := *message + if message.SupplyingAgencyMessage != nil { + copySam := *message.SupplyingAgencyMessage + copyMessage.SupplyingAgencyMessage = ©Sam + i.unifyItem(copyMessage.SupplyingAgencyMessage) + } + return ©Message +} + +func (i *Iso18626ReShareShim) setItemId(sam *iso18626.SupplyingAgencyMessage) { + if common.SamHasItems(*sam) { + result, startIdx, endIdx := common.GetItemParams(sam.MessageInfo.Note) + if len(result) == 1 { + sam.DeliveryInfo.ItemId = strings.Join(result[0], ",") + } else { + var items []string + for _, item := range result { + items = append(items, strings.Join(item, ",")) + } + sam.DeliveryInfo.ItemId = "multivol:" + strings.Join(items, ",multivol:") + } + tillIndex := max(0, startIdx-1) // -1 because we remove new line symbol but index cannot be negative + sam.MessageInfo.Note = sam.MessageInfo.Note[0:tillIndex] + + sam.MessageInfo.Note[endIdx+len(common.MULTIPLE_ITEMS_END):] + } +} + +func (i *Iso18626ReShareShim) unifyItem(sam *iso18626.SupplyingAgencyMessage) { + if sam.DeliveryInfo != nil && sam.DeliveryInfo.ItemId != "" { + var sb strings.Builder + //retain original note + if sam.MessageInfo.Note != "" { + sb.WriteString(sam.MessageInfo.Note) + sb.WriteString("\n") + } + sb.WriteString(common.MULTIPLE_ITEMS) + sb.WriteString("\n") + if strings.Contains(sam.DeliveryInfo.ItemId, "multivol:") { + list := strings.Split(sam.DeliveryInfo.ItemId, ",multivol:") + for _, item := range list { + item = strings.Replace(item, "multivol:", "", 1) + writeItemValues(&sb, item) + sb.WriteString("\n") + } + } else { + writeItemValues(&sb, sam.DeliveryInfo.ItemId) + sb.WriteString("\n") + } + sb.WriteString(common.MULTIPLE_ITEMS_END) + sam.MessageInfo.Note = sb.String() + } +} + +func writeItemValues(sb *strings.Builder, itemId string) { + match := reshareItemRegex.FindStringSubmatch(itemId) + var row string + if len(match) > 0 { + row = common.PackItemsNote([]string{match[1], match[2], match[3]}) + } else { + row = common.PackItemsNote([]string{itemId}) + } + sb.WriteString(row) +} diff --git a/broker/shim/shim_test.go b/broker/shim/shim_test.go index fffa6bdc..08d47c28 100644 --- a/broker/shim/shim_test.go +++ b/broker/shim/shim_test.go @@ -757,3 +757,188 @@ func TestAppendUnfilledStatusAndReasonUnfilled(t *testing.T) { shima.appendUnfilledStatusAndReasonUnfilled(&sam) assert.Equal(t, "", sam.MessageInfo.Note) } + +func TestProcessItemIdOneItemSimpleReShare(t *testing.T) { + shimR := new(Iso18626ReShareShim) + + sam := iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + DeliveryInfo: &iso18626.DeliveryInfo{ + ItemId: "1|23\\", + }, + MessageInfo: iso18626.MessageInfo{ + Note: "send one item", + }, + }, + } + + result := shimR.ApplyToIncomingRequest(&sam, nil, nil) + assert.Equal(t, "1|23\\", result.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send one item\n#MultipleItems#\n1\\|23\\\\\n#MultipleItemsEnd#", result.SupplyingAgencyMessage.MessageInfo.Note) + + result.SupplyingAgencyMessage.DeliveryInfo.ItemId = "" + mesBytes, err := shimR.ApplyToOutgoingRequest(result) + assert.NoError(t, err) + var resmsg iso18626.ISO18626Message + err = new(Iso18626DefaultShim).ApplyToIncomingResponse(mesBytes, &resmsg) + assert.NoError(t, err) + assert.Equal(t, "1|23\\", resmsg.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send one item", resmsg.SupplyingAgencyMessage.MessageInfo.Note) +} + +func TestProcessItemIdOneItemReShare(t *testing.T) { + shimR := new(Iso18626ReShareShim) + + sam := iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + DeliveryInfo: &iso18626.DeliveryInfo{ + ItemId: "name1|2,3\\,c|123,id|123", + }, + MessageInfo: iso18626.MessageInfo{ + Note: "send one item", + }, + }, + } + + result := shimR.ApplyToIncomingRequest(&sam, nil, nil) + assert.Equal(t, "name1|2,3\\,c|123,id|123", result.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send one item\n"+ + "#MultipleItems#\n"+ + "name1\\|2,3\\\\|c\\|123|id\\|123\n"+ + "#MultipleItemsEnd#", result.SupplyingAgencyMessage.MessageInfo.Note) + + result.SupplyingAgencyMessage.DeliveryInfo.ItemId = "" + mesBytes, err := shimR.ApplyToOutgoingRequest(result) + assert.NoError(t, err) + var resmsg iso18626.ISO18626Message + err = new(Iso18626DefaultShim).ApplyToIncomingResponse(mesBytes, &resmsg) + assert.NoError(t, err) + assert.Equal(t, "name1|2,3\\,c|123,id|123", resmsg.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send one item", resmsg.SupplyingAgencyMessage.MessageInfo.Note) +} + +func TestProcessItemIdMultipleItemsReShare(t *testing.T) { + shimR := new(Iso18626ReShareShim) + + sam := iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + DeliveryInfo: &iso18626.DeliveryInfo{ + ItemId: "multivol:name1|2,3\\,c|123,id|123,multivol:name1|2,4\\,c|124,id|124,multivol:name1|2,5\\,c|125,id|125", + }, + MessageInfo: iso18626.MessageInfo{ + Note: "send multiple items", + }, + }, + } + + result := shimR.ApplyToIncomingRequest(&sam, nil, nil) + assert.Equal(t, "multivol:name1|2,3\\,c|123,id|123,multivol:name1|2,4\\,c|124,id|124,multivol:name1|2,5\\,c|125,id|125", result.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send multiple items\n"+ + "#MultipleItems#\n"+ + "name1\\|2,3\\\\|c\\|123|id\\|123\n"+ + "name1\\|2,4\\\\|c\\|124|id\\|124\n"+ + "name1\\|2,5\\\\|c\\|125|id\\|125\n"+ + "#MultipleItemsEnd#", result.SupplyingAgencyMessage.MessageInfo.Note) + + result.SupplyingAgencyMessage.DeliveryInfo.ItemId = "" + mesBytes, err := shimR.ApplyToOutgoingRequest(result) + assert.NoError(t, err) + var resmsg iso18626.ISO18626Message + err = new(Iso18626DefaultShim).ApplyToIncomingResponse(mesBytes, &resmsg) + assert.NoError(t, err) + assert.Equal(t, "multivol:name1|2,3\\,c|123,id|123,multivol:name1|2,4\\,c|124,id|124,multivol:name1|2,5\\,c|125,id|125", resmsg.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send multiple items", resmsg.SupplyingAgencyMessage.MessageInfo.Note) +} + +func TestProcessItemIdMultipleItemsReShareNoNote(t *testing.T) { + shimR := new(Iso18626ReShareShim) + + sam := iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + DeliveryInfo: &iso18626.DeliveryInfo{ + ItemId: "multivol:name1|2,3\\,c|123,id|123,multivol:name1|2,4\\,c|124,id|124,multivol:name1|2,5\\,c|125,id|125", + }, + }, + } + + result := shimR.ApplyToIncomingRequest(&sam, nil, nil) + assert.Equal(t, "multivol:name1|2,3\\,c|123,id|123,multivol:name1|2,4\\,c|124,id|124,multivol:name1|2,5\\,c|125,id|125", result.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "#MultipleItems#\n"+ + "name1\\|2,3\\\\|c\\|123|id\\|123\n"+ + "name1\\|2,4\\\\|c\\|124|id\\|124\n"+ + "name1\\|2,5\\\\|c\\|125|id\\|125\n"+ + "#MultipleItemsEnd#", result.SupplyingAgencyMessage.MessageInfo.Note) + + result.SupplyingAgencyMessage.DeliveryInfo.ItemId = "" + mesBytes, err := shimR.ApplyToOutgoingRequest(result) + assert.NoError(t, err) + var resmsg iso18626.ISO18626Message + err = new(Iso18626DefaultShim).ApplyToIncomingResponse(mesBytes, &resmsg) + assert.NoError(t, err) + assert.Equal(t, "multivol:name1|2,3\\,c|123,id|123,multivol:name1|2,4\\,c|124,id|124,multivol:name1|2,5\\,c|125,id|125", resmsg.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "", resmsg.SupplyingAgencyMessage.MessageInfo.Note) +} + +func TestProcessItemIdOneItemAlma(t *testing.T) { + shimA := new(Iso18626AlmaShim) + + sam := iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + DeliveryInfo: &iso18626.DeliveryInfo{ + ItemId: "1|23\\", + }, + MessageInfo: iso18626.MessageInfo{ + Note: "send one item", + }, + }, + } + + result := shimA.ApplyToIncomingRequest(&sam, nil, nil) + assert.Equal(t, "1|23\\", result.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send one item\n"+ + "#MultipleItems#\n"+ + "1\\|23\\\\\n"+ + "#MultipleItemsEnd#", result.SupplyingAgencyMessage.MessageInfo.Note) + + result.SupplyingAgencyMessage.DeliveryInfo.ItemId = "" + mesBytes, err := shimA.ApplyToOutgoingRequest(result) + assert.NoError(t, err) + var resmsg iso18626.ISO18626Message + err = new(Iso18626DefaultShim).ApplyToIncomingResponse(mesBytes, &resmsg) + assert.NoError(t, err) + assert.Equal(t, "1|23\\", resmsg.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send one item", resmsg.SupplyingAgencyMessage.MessageInfo.Note) +} + +func TestProcessItemIdMultipleItemsAlma(t *testing.T) { + shimA := new(Iso18626AlmaShim) + + sam := iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + DeliveryInfo: &iso18626.DeliveryInfo{ + ItemId: "1|23\\,1|24\\,1|25\\", + }, + MessageInfo: iso18626.MessageInfo{ + Note: "send multiple items", + }, + }, + } + + result := shimA.ApplyToIncomingRequest(&sam, nil, nil) + assert.Equal(t, "1|23\\,1|24\\,1|25\\", result.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send multiple items\n"+ + "#MultipleItems#\n"+ + "1\\|23\\\\\n"+ + "1\\|24\\\\\n"+ + "1\\|25\\\\\n"+ + "#MultipleItemsEnd#", result.SupplyingAgencyMessage.MessageInfo.Note) + + result.SupplyingAgencyMessage.DeliveryInfo.ItemId = "" + mesBytes, err := shimA.ApplyToOutgoingRequest(result) + assert.NoError(t, err) + var resmsg iso18626.ISO18626Message + err = new(Iso18626DefaultShim).ApplyToIncomingResponse(mesBytes, &resmsg) + assert.NoError(t, err) + assert.Equal(t, "1|23\\,1|24\\,1|25\\", resmsg.SupplyingAgencyMessage.DeliveryInfo.ItemId) + assert.Equal(t, "send multiple items", resmsg.SupplyingAgencyMessage.MessageInfo.Note) +} diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index d6e6ceac..4deef4ad 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -378,6 +378,13 @@ func TestActionsToCompleteState(t *testing.T) { assert.NoError(t, err, "failed to unmarshal patron request events") assert.True(t, len(events) > 5) + // Check requester patron request item count + respBytes = httpRequest(t, "GET", requesterPrPath+"/items"+queryParams, []byte{}, 200) + var prItems []proapi.PrItem + err = json.Unmarshal(respBytes, &prItems) + assert.NoError(t, err, "failed to unmarshal patron request items") + assert.Len(t, prItems, 0) + // Check supplier patron request done respBytes = httpRequest(t, "GET", supplierPrPath+supQueryParams, []byte{}, 200) err = json.Unmarshal(respBytes, &foundPr) diff --git a/broker/test/patron_request/db/prrepo_test.go b/broker/test/patron_request/db/prrepo_test.go index a30fff9a..7d550182 100644 --- a/broker/test/patron_request/db/prrepo_test.go +++ b/broker/test/patron_request/db/prrepo_test.go @@ -149,7 +149,7 @@ func TestItem(t *testing.T) { assert.Equal(t, itemId, item.ID) // Get by pr id - items, err := prRepo.GetItemByPrId(appCtx, prId) + items, err := prRepo.GetItemsByPrId(appCtx, prId) assert.NoError(t, err) assert.Len(t, items, 1) assert.Equal(t, itemId, items[0].ID)