diff --git a/broker/README.md b/broker/README.md index 9af516a1..772b155c 100644 --- a/broker/README.md +++ b/broker/README.md @@ -57,38 +57,39 @@ Note that for all modes, the broker attaches Directory information about the sup Configuration is provided via environment variables: -| Name | Description | Default value | -|------------------------|-------------------------------------------------------------------------------------------|-------------------------------------------| -| `HTTP_PORT` | Server port | `8081` | -| `DB_TYPE` | Database type | `postgres` | -| `DB_USER` | Database user | `crosslink` | -| `DB_PASSWORD` | Database password | `crosslink` | -| `DB_HOST` | Database host | `localhost` | -| `DB_DATABASE` | Database name | `crosslink` | -| `DB_PORT` | Database port | `25432` | -| `LOG_LEVEL` | Log level: `ERROR`, `WARN`, `INFO`, `DEBUG` | `INFO` | -| `ENABLE_JSON_LOG` | Should JSON log format be enabled | `false` | -| `BROKER_MODE` | Default broker mode if not configured for a peer: `opaque` or `transparent` | `opaque` | -| `BROKER_SYMBOL` | Symbol for the broker when in the `opaque` mode | `ISIL:BROKER` | -| `REQ_AGENCY_INFO` | Should `request/requestingAgencyInfo` be populated from Directory | `true` | -| `SUPPLIER_INFO` | Should `request/supplierInfo` be populated from Directory | `true` | -| `RETURN_INFO` | Should `returnInfo` be populated from Directory for supplier `Loaned` message | `true` | -| `VENDOR_NOTE` | Should `note` field be prepended with `Vendor: {vendor}` text | `true` | -| `SUPPLIER_SYMBOL_NOTE` | Should `note` field be prepended with a `Supplier: {symbol}` text, `opaque` mode only | `true` | -| `OFFERED_COSTS` | Should `deliveryCosts` be transferred to `offeredCosts` for ReShare vendor requesters | `false` | -| `NOTE_FIELD_SEP` | Separator for fields (e.g. Vendor) prepended to the note | `, ` | -| `CLIENT_DELAY` | Delay duration for outgoing ISO18626 messages | `0ms` | -| `SHUTDOWN_DELAY` | Delay duration for graceful shutdown (in-flight connections) | `15s` | -| `MAX_MESSAGE_SIZE` | Max accepted ISO18626 message size | `100KB` | -| `HOLDINGS_ADAPTER` | Holdings lookup method: `mock` or `sru` | `mock` | -| `SRU_URL` | Comma separated list of URLs when `HOLDINGS_ADAPTER` is `sru` | `http://localhost:8081/sru` | -| `DIRECTORY_ADAPTER` | Directory lookup method: `mock` or `api` | `mock` | -| `DIRECTORY_API_URL` | Comma separated list of URLs when `DIRECTORY_ADAPTER` is `api` | `http://localhost:8081/directory/entries` | -| `PEER_REFRESH_INTERVAL`| Peer refresh interval (via Directory lookup) | `5m` | -| `MOCK_CLIENT_URL` | Mocked peer URLs value when `DIRECTORY_ADAPTER` is `mock` | `http://localhost:19083/iso18626` | -| `API_PAGE_SIZE` | Default value for the `limit` query parameter when paging the API | `10` | -| `TENANT_TO_SYMBOL` | Pattern to map tenant to `requesterSymbol` when accessing the API via Okapi, | (empty value) | -| | the `{tenant}` token is replaced by the `X-Okapi-Tenant` header value | | +| Name | Description | Default value | +|--------------------------|---------------------------------------------------------------------------------------|-------------------------------------------| +| `HTTP_PORT` | Server port | `8081` | +| `DB_TYPE` | Database type | `postgres` | +| `DB_USER` | Database user | `crosslink` | +| `DB_PASSWORD` | Database password | `crosslink` | +| `DB_HOST` | Database host | `localhost` | +| `DB_DATABASE` | Database name | `crosslink` | +| `DB_PORT` | Database port | `25432` | +| `LOG_LEVEL` | Log level: `ERROR`, `WARN`, `INFO`, `DEBUG` | `INFO` | +| `ENABLE_JSON_LOG` | Should JSON log format be enabled | `false` | +| `BROKER_MODE` | Default broker mode if not configured for a peer: `opaque` or `transparent` | `opaque` | +| `BROKER_SYMBOL` | Symbol for the broker when in the `opaque` mode | `ISIL:BROKER` | +| `REQ_AGENCY_INFO` | Should `request/requestingAgencyInfo` be populated from Directory | `true` | +| `SUPPLIER_INFO` | Should `request/supplierInfo` be populated from Directory | `true` | +| `RETURN_INFO` | Should `returnInfo` be populated from Directory for supplier `Loaned` message | `true` | +| `VENDOR_NOTE` | Should `note` field be prepended with `Vendor: {vendor}` text | `true` | +| `SUPPLIER_SYMBOL_NOTE` | Should `note` field be prepended with a `Supplier: {symbol}` text, `opaque` mode only | `true` | +| `OFFERED_COSTS` | Should `deliveryCosts` be transferred to `offeredCosts` for ReShare vendor requesters | `false` | +| `NOTE_FIELD_SEP` | Separator for fields (e.g. Vendor) prepended to the note | `, ` | +| `CLIENT_DELAY` | Delay duration for outgoing ISO18626 messages | `0ms` | +| `SHUTDOWN_DELAY` | Delay duration for graceful shutdown (in-flight connections) | `15s` | +| `MAX_MESSAGE_SIZE` | Max accepted ISO18626 message size | `100KB` | +| `HOLDINGS_ADAPTER` | Holdings lookup method: `mock` or `sru` | `mock` | +| `SRU_URL` | Comma separated list of URLs when `HOLDINGS_ADAPTER` is `sru` | `http://localhost:8081/sru` | +| `DIRECTORY_ADAPTER` | Directory lookup method: `mock` or `api` | `mock` | +| `DIRECTORY_API_URL` | Comma separated list of URLs when `DIRECTORY_ADAPTER` is `api` | `http://localhost:8081/directory/entries` | +| `PEER_REFRESH_INTERVAL` | Peer refresh interval (via Directory lookup) | `5m` | +| `MOCK_CLIENT_URL` | Mocked peer URLs value when `DIRECTORY_ADAPTER` is `mock` | `http://localhost:19083/iso18626` | +| `API_PAGE_SIZE` | Default value for the `limit` query parameter when paging the API | `10` | +| `TENANT_TO_SYMBOL` | Pattern to map tenant to `requesterSymbol` when accessing the API via Okapi, | (empty value) | +| | the `{tenant}` token is replaced by the `X-Okapi-Tenant` header value | | +| `SUPPLIER_PATRON_PATTERN`| Pattern used to create patron ID when receiving Request on supplier side | `%v_user` | # Build diff --git a/broker/events/eventmodels.go b/broker/events/eventmodels.go index 2332eb3c..ef4b7080 100644 --- a/broker/events/eventmodels.go +++ b/broker/events/eventmodels.go @@ -1,6 +1,7 @@ package events import ( + pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/httpclient" "github.com/indexdata/crosslink/iso18626" ) @@ -61,13 +62,13 @@ type EventData struct { } type CommonEventData struct { - IncomingMessage *iso18626.ISO18626Message `json:"incomingMessage,omitempty"` - OutgoingMessage *iso18626.ISO18626Message `json:"outgoingMessage,omitempty"` - Problem *Problem `json:"problem,omitempty"` - HttpFailure *httpclient.HttpError `json:"httpFailure,omitempty"` - EventError *EventError `json:"eventError,omitempty"` - Note string `json:"note,omitempty"` - Action *string `json:"action,omitempty"` + IncomingMessage *iso18626.ISO18626Message `json:"incomingMessage,omitempty"` + OutgoingMessage *iso18626.ISO18626Message `json:"outgoingMessage,omitempty"` + Problem *Problem `json:"problem,omitempty"` + HttpFailure *httpclient.HttpError `json:"httpFailure,omitempty"` + EventError *EventError `json:"eventError,omitempty"` + Note string `json:"note,omitempty"` + Action *pr_db.PatronRequestAction `json:"action,omitempty"` } type EventError struct { diff --git a/broker/handler/iso18626-handler.go b/broker/handler/iso18626-handler.go index 416aedf8..11aa3c32 100644 --- a/broker/handler/iso18626-handler.go +++ b/broker/handler/iso18626-handler.go @@ -69,6 +69,7 @@ var waitingReqs = map[string]RequestWait{} type Iso18626HandlerInterface interface { HandleRequest(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter) HandleRequestingAgencyMessage(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter) + HandleSupplyingAgencyMessage(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter) } type Iso18626Handler struct { @@ -528,6 +529,9 @@ func handleRequestingAgencyErrorWithNotice(ctx common.ExtendedContext, w http.Re } } +func (h *Iso18626Handler) HandleSupplyingAgencyMessage(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter) { + handleSupplyingAgencyMessage(ctx, illMessage, w, h.illRepo, h.eventBus) +} func handleSupplyingAgencyMessage(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter, repo ill_db.IllRepo, eventBus events.EventBus) { var requestingRequestId = illMessage.SupplyingAgencyMessage.Header.RequestingAgencyRequestId if requestingRequestId == "" { diff --git a/broker/migrations/015_add_properties_to_patron_request.down.sql b/broker/migrations/015_add_properties_to_patron_request.down.sql new file mode 100644 index 00000000..e05133ed --- /dev/null +++ b/broker/migrations/015_add_properties_to_patron_request.down.sql @@ -0,0 +1,2 @@ + +ALTER TABLE patron_request DROP COLUMN requester_req_id; diff --git a/broker/migrations/015_add_properties_to_patron_request.up.sql b/broker/migrations/015_add_properties_to_patron_request.up.sql new file mode 100644 index 00000000..4018f051 --- /dev/null +++ b/broker/migrations/015_add_properties_to_patron_request.up.sql @@ -0,0 +1,2 @@ + +ALTER TABLE patron_request ADD COLUMN requester_req_id VARCHAR; diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index 8d6aa2ca..64a737ab 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -20,14 +20,16 @@ import ( var waitingReqs = map[string]RequestWait{} type PatronRequestApiHandler struct { - prRepo pr_db.PrRepo - eventBus events.EventBus + prRepo pr_db.PrRepo + eventBus events.EventBus + actionMappingService prservice.ActionMappingService } func NewApiHandler(prRepo pr_db.PrRepo, eventBus events.EventBus) PatronRequestApiHandler { return PatronRequestApiHandler{ - prRepo: prRepo, - eventBus: eventBus, + prRepo: prRepo, + eventBus: eventBus, + actionMappingService: prservice.ActionMappingService{}, } } @@ -68,8 +70,8 @@ func (a *PatronRequestApiHandler) PostPatronRequests(w http.ResponseWriter, r *h addInternalError(ctx, w, err) return } - - _, err = a.eventBus.CreateTask(pr.ID, events.EventNameInvokeAction, events.EventData{CommonEventData: events.CommonEventData{Action: &prservice.ActionValidate}}, events.EventDomainPatronRequest, nil) + action := prservice.BorrowerActionValidate + _, err = a.eventBus.CreateTask(pr.ID, events.EventNameInvokeAction, events.EventData{CommonEventData: events.CommonEventData{Action: &action}}, events.EventDomainPatronRequest, nil) if err != nil { addInternalError(ctx, w, err) return @@ -133,7 +135,8 @@ func (a *PatronRequestApiHandler) GetPatronRequestsIdActions(w http.ResponseWrit return } } - writeJsonResponse(w, prservice.GetBorrowerActionsByState(pr.State)) + actions := a.actionMappingService.GetActionMapping(pr).GetActionsForPatronRequest(pr) + writeJsonResponse(w, actions) } func (a *PatronRequestApiHandler) PostPatronRequestsIdAction(w http.ResponseWriter, r *http.Request, id string, params proapi.PostPatronRequestsIdActionParams) { @@ -156,12 +159,13 @@ func (a *PatronRequestApiHandler) PostPatronRequestsIdAction(w http.ResponseWrit return } } - if !prservice.IsBorrowerActionAvailable(pr.State, action.Action) { + + if !a.actionMappingService.GetActionMapping(pr).IsActionAvailable(pr, pr_db.PatronRequestAction(action.Action)) { addBadRequestError(ctx, w, errors.New("Action "+action.Action+" is not allowed for patron request "+id)) return } - - data := events.EventData{CommonEventData: events.CommonEventData{Action: &action.Action}} + eventAction := pr_db.PatronRequestAction(action.Action) + data := events.EventData{CommonEventData: events.CommonEventData{Action: &eventAction}} if action.ActionParams != nil { data.CustomData = *action.ActionParams } @@ -232,8 +236,8 @@ func toApiPatronRequest(request pr_db.PatronRequest) proapi.PatronRequest { return proapi.PatronRequest{ ID: request.ID, Timestamp: request.Timestamp.Time, - State: request.State, - Side: request.Side, + State: string(request.State), + Side: string(request.Side), Patron: toString(request.Patron), RequesterSymbol: toString(request.RequesterSymbol), SupplierSymbol: toString(request.SupplierSymbol), diff --git a/broker/patron_request/api/api-handler_test.go b/broker/patron_request/api/api-handler_test.go index 5a4aa500..4adb9174 100644 --- a/broker/patron_request/api/api-handler_test.go +++ b/broker/patron_request/api/api-handler_test.go @@ -119,6 +119,7 @@ func TestGetPatronRequestsId(t *testing.T) { type PrRepoError struct { mock.Mock + pr_db.PgPrRepo } func (r *PrRepoError) WithTxFunc(ctx common.ExtendedContext, fn func(repo pr_db.PrRepo) error) error { diff --git a/broker/patron_request/db/prmodels.go b/broker/patron_request/db/prmodels.go new file mode 100644 index 00000000..a903bbc4 --- /dev/null +++ b/broker/patron_request/db/prmodels.go @@ -0,0 +1,5 @@ +package pr_db + +type PatronRequestState string +type PatronRequestSide string +type PatronRequestAction string diff --git a/broker/patron_request/db/prrepo.go b/broker/patron_request/db/prrepo.go index b1988b30..86c7729e 100644 --- a/broker/patron_request/db/prrepo.go +++ b/broker/patron_request/db/prrepo.go @@ -3,6 +3,7 @@ package pr_db import ( "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/repo" + "github.com/jackc/pgx/v5/pgtype" ) type PrRepo interface { @@ -11,6 +12,7 @@ type PrRepo interface { ListPatronRequests(ctx common.ExtendedContext) ([]PatronRequest, error) SavePatronRequest(ctx common.ExtendedContext, params SavePatronRequestParams) (PatronRequest, error) DeletePatronRequest(ctx common.ExtendedContext, id string) error + GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx common.ExtendedContext, supplierSymbol string, requesterReId string) (PatronRequest, error) } type PgPrRepo struct { @@ -54,3 +56,17 @@ func (r *PgPrRepo) SavePatronRequest(ctx common.ExtendedContext, params SavePatr func (r *PgPrRepo) DeletePatronRequest(ctx common.ExtendedContext, id string) error { return r.queries.DeletePatronRequest(ctx, r.GetConnOrTx(), id) } + +func (r *PgPrRepo) GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx common.ExtendedContext, supplierSymbol string, requesterReId string) (PatronRequest, error) { + row, err := r.queries.GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx, r.GetConnOrTx(), GetPatronRequestBySupplierSymbolAndRequesterReqIdParams{ + SupplierSymbol: pgtype.Text{ + String: supplierSymbol, + Valid: true, + }, + RequesterReqID: pgtype.Text{ + String: requesterReId, + Valid: true, + }, + }) + return row.PatronRequest, err +} diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index f196f82c..00f95f58 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -1,90 +1,90 @@ package prservice import ( + "encoding/json" "encoding/xml" "errors" - "net/http" - "slices" - "strings" - "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/handler" "github.com/indexdata/crosslink/broker/ill_db" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" + "net/http" + "strings" ) const COMP = "pr_action_service" -var SideBorrowing = "borrowing" -var SideLanding = "landing" - -var BorrowerStateNew = "NEW" -var BorrowerStateValidated = "VALIDATED" -var BorrowerStateSent = "SENT" -var BorrowerStateSupplierLocated = "SUPPLIER_LOCATED" -var BorrowerStateConditionPending = "CONDITION_PENDING" -var BorrowerStateWillSupply = "WILL_SUPPLY" -var BorrowerStateShipped = "SHIPPED" -var BorrowerStateReceived = "RECEIVED" -var BorrowerStateCheckedOut = "CHECKED_OUT" -var BorrowerStateCheckedIn = "CHECKED_IN" -var BorrowerStateShippedReturned = "SHIPPED_RETURNED" -var BorrowerStateCancelPending = "CANCEL_PENDING" -var BorrowerStateCompleted = "COMPLETED" -var BorrowerStateCancelled = "CANCELLED" -var BorrowerStateUnfilled = "UNFILLED" - -var ActionValidate = "validate" -var ActionSendRequest = "send-request" -var ActionCancelRequest = "cancel-request" -var ActionAcceptCondition = "accept-condition" -var ActionRejectCondition = "reject-condition" -var ActionReceive = "receive" -var ActionCheckOut = "check-out" -var ActionCheckIn = "check-in" -var ActionShipReturn = "ship-return" - -var BorrowerStateActionMapping = map[string][]string{ - BorrowerStateNew: {ActionValidate}, - BorrowerStateValidated: {ActionSendRequest}, - BorrowerStateSupplierLocated: {ActionCancelRequest}, - BorrowerStateConditionPending: {ActionAcceptCondition, ActionRejectCondition}, - BorrowerStateWillSupply: {ActionCancelRequest}, - BorrowerStateShipped: {ActionReceive}, - BorrowerStateReceived: {ActionCheckOut}, - BorrowerStateCheckedOut: {ActionCheckIn}, - BorrowerStateCheckedIn: {ActionShipReturn}, -} +const ( + SideBorrowing pr_db.PatronRequestSide = "borrowing" + SideLending pr_db.PatronRequestSide = "lending" + BorrowerStateNew pr_db.PatronRequestState = "NEW" + BorrowerStateValidated pr_db.PatronRequestState = "VALIDATED" + BorrowerStateSent pr_db.PatronRequestState = "SENT" + BorrowerStateSupplierLocated pr_db.PatronRequestState = "SUPPLIER_LOCATED" + BorrowerStateConditionPending pr_db.PatronRequestState = "CONDITION_PENDING" + BorrowerStateWillSupply pr_db.PatronRequestState = "WILL_SUPPLY" + BorrowerStateShipped pr_db.PatronRequestState = "SHIPPED" + BorrowerStateReceived pr_db.PatronRequestState = "RECEIVED" + BorrowerStateCheckedOut pr_db.PatronRequestState = "CHECKED_OUT" + BorrowerStateCheckedIn pr_db.PatronRequestState = "CHECKED_IN" + BorrowerStateShippedReturned pr_db.PatronRequestState = "SHIPPED_RETURNED" + BorrowerStateCancelPending pr_db.PatronRequestState = "CANCEL_PENDING" + BorrowerStateCompleted pr_db.PatronRequestState = "COMPLETED" + BorrowerStateCancelled pr_db.PatronRequestState = "CANCELLED" + BorrowerStateUnfilled pr_db.PatronRequestState = "UNFILLED" + LenderStateNew pr_db.PatronRequestState = "NEW" + LenderStateValidated pr_db.PatronRequestState = "VALIDATED" + LenderStateWillSupply pr_db.PatronRequestState = "WILL_SUPPLY" + LenderStateConditionPending pr_db.PatronRequestState = "CONDITION_PENDING" + LenderStateConditionAccepted pr_db.PatronRequestState = "CONDITION_ACCEPTED" + LenderStateShipped pr_db.PatronRequestState = "SHIPPED" + LenderStateShippedReturn pr_db.PatronRequestState = "SHIPPED_RETURN" + LenderStateCancelRequested pr_db.PatronRequestState = "CANCEL_REQUESTED" + LenderStateCompleted pr_db.PatronRequestState = "COMPLETED" + LenderStateCancelled pr_db.PatronRequestState = "CANCELLED" + LenderStateUnfilled pr_db.PatronRequestState = "UNFILLED" +) + +const ( + BorrowerActionValidate pr_db.PatronRequestAction = "validate" + BorrowerActionSendRequest pr_db.PatronRequestAction = "send-request" + BorrowerActionCancelRequest pr_db.PatronRequestAction = "cancel-request" + BorrowerActionAcceptCondition pr_db.PatronRequestAction = "accept-condition" + BorrowerActionRejectCondition pr_db.PatronRequestAction = "reject-condition" + BorrowerActionReceive pr_db.PatronRequestAction = "receive" + BorrowerActionCheckOut pr_db.PatronRequestAction = "check-out" + BorrowerActionCheckIn pr_db.PatronRequestAction = "check-in" + BorrowerActionShipReturn pr_db.PatronRequestAction = "ship-return" + + LenderActionValidate pr_db.PatronRequestAction = "validate" + LenderActionWillSupply pr_db.PatronRequestAction = "will-supply" + LenderActionCannotSupply pr_db.PatronRequestAction = "cannot-supply" + LenderActionAddCondition pr_db.PatronRequestAction = "add-condition" + LenderActionShip pr_db.PatronRequestAction = "ship" + LenderActionMarkReceived pr_db.PatronRequestAction = "mark-received" + LenderActionMarkCancelled pr_db.PatronRequestAction = "mark-cancelled" +) type PatronRequestActionService struct { - prRepo pr_db.PrRepo - illRepo ill_db.IllRepo - eventBus events.EventBus - iso18626Handler handler.Iso18626HandlerInterface + prRepo pr_db.PrRepo + illRepo ill_db.IllRepo + eventBus events.EventBus + iso18626Handler handler.Iso18626HandlerInterface + actionMappingService ActionMappingService } func CreatePatronRequestActionService(prRepo pr_db.PrRepo, illRepo ill_db.IllRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface) PatronRequestActionService { return PatronRequestActionService{ - prRepo: prRepo, - illRepo: illRepo, - eventBus: eventBus, - iso18626Handler: iso18626Handler, - } -} -func GetBorrowerActionsByState(state string) []string { - if actions, ok := BorrowerStateActionMapping[state]; ok { - return actions - } else { - return []string{} + prRepo: prRepo, + illRepo: illRepo, + eventBus: eventBus, + iso18626Handler: iso18626Handler, + actionMappingService: ActionMappingService{}, } } -func IsBorrowerActionAvailable(state string, action string) bool { - return slices.Contains(GetBorrowerActionsByState(state), action) -} - func (a *PatronRequestActionService) InvokeAction(ctx common.ExtendedContext, event events.Event) { ctx = ctx.WithArgs(ctx.LoggerArgs().WithComponent(COMP)) _, _ = a.eventBus.ProcessTask(ctx, event, a.handleInvokeAction) @@ -99,34 +99,66 @@ func (a *PatronRequestActionService) handleInvokeAction(ctx common.ExtendedConte if err != nil { return events.LogErrorAndReturnResult(ctx, "failed to read patron request", err) } - if !IsBorrowerActionAvailable(pr.State, action) { - return events.LogErrorAndReturnResult(ctx, "state "+pr.State+" does not support action "+action, errors.New("invalid action")) + if !a.actionMappingService.GetActionMapping(pr).IsActionAvailable(pr, action) { + return events.LogErrorAndReturnResult(ctx, "state "+string(pr.State)+" does not support action "+string(action), errors.New("invalid action")) } + switch pr.Side { + case SideBorrowing: + return a.handleBorrowingAction(ctx, action, pr) + case SideLending: + return a.handleLenderAction(ctx, action, pr, event.EventData.CustomData) + default: + return events.LogErrorAndReturnResult(ctx, "side "+string(pr.Side)+" is not supported", errors.New("invalid side")) + } +} +func (a *PatronRequestActionService) handleBorrowingAction(ctx common.ExtendedContext, action pr_db.PatronRequestAction, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { switch action { - case ActionValidate: + case BorrowerActionValidate: return a.validateBorrowingRequest(ctx, pr) - case ActionSendRequest: + case BorrowerActionSendRequest: return a.sendBorrowingRequest(ctx, pr) - case ActionReceive: + case BorrowerActionReceive: return a.receiveBorrowingRequest(ctx, pr) - case ActionCheckOut: + case BorrowerActionCheckOut: return a.checkoutBorrowingRequest(ctx, pr) - case ActionCheckIn: + case BorrowerActionCheckIn: return a.checkinBorrowingRequest(ctx, pr) - case ActionShipReturn: + case BorrowerActionShipReturn: return a.shipReturnBorrowingRequest(ctx, pr) - case ActionCancelRequest: + case BorrowerActionCancelRequest: return a.cancelBorrowingRequest(ctx, pr) - case ActionAcceptCondition: + case BorrowerActionAcceptCondition: return a.acceptConditionBorrowingRequest(ctx, pr) - case ActionRejectCondition: + case BorrowerActionRejectCondition: return a.rejectConditionBorrowingRequest(ctx, pr) default: - return events.LogErrorAndReturnResult(ctx, "action "+action+" is not implemented yet", errors.New("invalid action")) + return events.LogErrorAndReturnResult(ctx, "borrower action "+string(action)+" is not implemented yet", errors.New("invalid action")) + } +} + +func (a *PatronRequestActionService) handleLenderAction(ctx common.ExtendedContext, action pr_db.PatronRequestAction, pr pr_db.PatronRequest, actionParams map[string]interface{}) (events.EventStatus, *events.EventResult) { + switch action { + case LenderActionValidate: + return a.validateLenderRequest(ctx, pr) + case LenderActionWillSupply: + return a.willSupplyLenderRequest(ctx, pr) + case LenderActionCannotSupply: + return a.cannotSupplyLenderRequest(ctx, pr) + case LenderActionAddCondition: + return a.addConditionsLenderRequest(ctx, pr, actionParams) + case LenderActionShip: + return a.shipLenderRequest(ctx, pr) + case LenderActionMarkReceived: + return a.markReceivedLenderRequest(ctx, pr) + case LenderActionMarkCancelled: + return a.markCancelledLenderRequest(ctx, pr) + default: + return events.LogErrorAndReturnResult(ctx, "lender action "+string(action)+" is not implemented yet", errors.New("invalid action")) } } -func (a *PatronRequestActionService) updateStateAndReturnResult(ctx common.ExtendedContext, pr pr_db.PatronRequest, state string, result *events.EventResult) (events.EventStatus, *events.EventResult) { + +func (a *PatronRequestActionService) updateStateAndReturnResult(ctx common.ExtendedContext, pr pr_db.PatronRequest, state pr_db.PatronRequestState, result *events.EventResult) (events.EventStatus, *events.EventResult) { pr.State = state pr, err := a.prRepo.SavePatronRequest(ctx, pr_db.SavePatronRequestParams(pr)) if err != nil { @@ -134,6 +166,16 @@ func (a *PatronRequestActionService) updateStateAndReturnResult(ctx common.Exten } return events.EventStatusSuccess, result } +func (a *PatronRequestActionService) checkSupplyingResponseAndUpdateState(ctx common.ExtendedContext, pr pr_db.PatronRequest, state pr_db.PatronRequestState, result *events.EventResult, status events.EventStatus, eventResult *events.EventResult, httpStatus *int) (events.EventStatus, *events.EventResult) { + if httpStatus == nil { + return status, eventResult + } + if *httpStatus != http.StatusOK || result.IncomingMessage == nil || result.IncomingMessage.SupplyingAgencyMessageConfirmation == nil || + result.IncomingMessage.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus != iso18626.TypeMessageStatusOK { + return events.EventStatusProblem, result + } + return a.updateStateAndReturnResult(ctx, pr, state, nil) +} func (a *PatronRequestActionService) validateBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { if !pr.Tenant.Valid { @@ -151,6 +193,12 @@ func (a *PatronRequestActionService) sendBorrowingRequest(ctx common.ExtendedCon if len(requesterSymbol) != 2 { return events.LogErrorAndReturnResult(ctx, "invalid requester symbol", nil) } + + var request *iso18626.Request + err := json.Unmarshal(pr.IllRequest, &request) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to parse request", err) + } var illMessage = iso18626.ISO18626Message{ Request: &iso18626.Request{ Header: iso18626.Header{ @@ -162,10 +210,8 @@ func (a *PatronRequestActionService) sendBorrowingRequest(ctx common.ExtendedCon }, RequestingAgencyRequestId: pr.ID, }, - PatronInfo: &iso18626.PatronInfo{PatronId: pr.Patron.String}, - BibliographicInfo: iso18626.BibliographicInfo{ - SupplierUniqueRecordId: "WILLSUPPLY_LOANED", - }, + PatronInfo: &iso18626.PatronInfo{PatronId: pr.Patron.String}, + BibliographicInfo: request.BibliographicInfo, }, } w := NewResponseCaptureWriter() @@ -284,6 +330,93 @@ func (a *PatronRequestActionService) rejectConditionBorrowingRequest(ctx common. return a.updateStateAndReturnResult(ctx, pr, BorrowerStateCancelPending, nil) } +func (a *PatronRequestActionService) validateLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + // TODO do validation + + return a.updateStateAndReturnResult(ctx, pr, LenderStateValidated, nil) +} + +func (a *PatronRequestActionService) willSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageNotification}, iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}) + return a.checkSupplyingResponseAndUpdateState(ctx, pr, LenderStateWillSupply, &result, status, eventResult, httpStatus) +} + +func (a *PatronRequestActionService) cannotSupplyLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled}) + return a.checkSupplyingResponseAndUpdateState(ctx, pr, LenderStateUnfilled, &result, status, eventResult, httpStatus) +} + +func (a *PatronRequestActionService) addConditionsLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest, actionParams map[string]interface{}) (events.EventStatus, *events.EventResult) { + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, + iso18626.MessageInfo{ + ReasonForMessage: iso18626.TypeReasonForMessageNotification, + Note: RESHARE_ADD_LOAN_CONDITION, // TODO add action params + }, + iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}) + return a.checkSupplyingResponseAndUpdateState(ctx, pr, LenderStateConditionPending, &result, status, eventResult, httpStatus) +} + +func (a *PatronRequestActionService) shipLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusLoaned}) + return a.checkSupplyingResponseAndUpdateState(ctx, pr, LenderStateShipped, &result, status, eventResult, httpStatus) +} + +func (a *PatronRequestActionService) markReceivedLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusLoanCompleted}) + return a.checkSupplyingResponseAndUpdateState(ctx, pr, LenderStateCompleted, &result, status, eventResult, httpStatus) +} + +func (a *PatronRequestActionService) markCancelledLenderRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + result := events.EventResult{} + status, eventResult, httpStatus := a.sendSupplyingAgencyMessage(ctx, pr, &result, iso18626.MessageInfo{ReasonForMessage: iso18626.TypeReasonForMessageStatusChange}, iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled}) + return a.checkSupplyingResponseAndUpdateState(ctx, pr, LenderStateCancelled, &result, status, eventResult, httpStatus) +} + +func (a *PatronRequestActionService) sendSupplyingAgencyMessage(ctx common.ExtendedContext, pr pr_db.PatronRequest, result *events.EventResult, messageInfo iso18626.MessageInfo, statusInfo iso18626.StatusInfo) (events.EventStatus, *events.EventResult, *int) { + if !pr.RequesterSymbol.Valid { + status, eventResult := events.LogErrorAndReturnResult(ctx, "missing requester symbol", nil) + return status, eventResult, nil + } + if !pr.SupplierSymbol.Valid { + status, eventResult := events.LogErrorAndReturnResult(ctx, "missing supplier symbol", nil) + return status, eventResult, nil + } + requesterSymbol := strings.SplitN(pr.RequesterSymbol.String, ":", 2) + supplierSymbol := strings.SplitN(pr.SupplierSymbol.String, ":", 2) + var illMessage = iso18626.ISO18626Message{ + SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: requesterSymbol[0], + }, + AgencyIdValue: requesterSymbol[1], + }, + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: supplierSymbol[0], + }, + AgencyIdValue: supplierSymbol[1], + }, + RequestingAgencyRequestId: pr.RequesterReqID.String, + SupplyingAgencyRequestId: pr.ID, + }, + MessageInfo: messageInfo, + StatusInfo: statusInfo, + }, + } + w := NewResponseCaptureWriter() + a.iso18626Handler.HandleSupplyingAgencyMessage(ctx, &illMessage, w) + result.OutgoingMessage = &illMessage + result.IncomingMessage = w.IllMessage + return "", nil, &w.StatusCode +} + type ResponseCaptureWriter struct { IllMessage *iso18626.ISO18626Message StatusCode int diff --git a/broker/patron_request/service/action_mapping.go b/broker/patron_request/service/action_mapping.go new file mode 100644 index 00000000..548db4e5 --- /dev/null +++ b/broker/patron_request/service/action_mapping.go @@ -0,0 +1,68 @@ +package prservice + +import ( + pr_db "github.com/indexdata/crosslink/broker/patron_request/db" + "slices" +) + +type ActionMapping interface { + IsActionAvailable(pr pr_db.PatronRequest, action pr_db.PatronRequestAction) bool + GetActionsForPatronRequest(pr pr_db.PatronRequest) []pr_db.PatronRequestAction +} + +var returnableBorrowerStateActionMapping = map[pr_db.PatronRequestState][]pr_db.PatronRequestAction{ + BorrowerStateNew: {BorrowerActionValidate}, + BorrowerStateValidated: {BorrowerActionSendRequest}, + BorrowerStateSupplierLocated: {BorrowerActionCancelRequest}, + BorrowerStateConditionPending: {BorrowerActionAcceptCondition, BorrowerActionRejectCondition}, + BorrowerStateWillSupply: {BorrowerActionCancelRequest}, + BorrowerStateShipped: {BorrowerActionReceive}, + BorrowerStateReceived: {BorrowerActionCheckOut}, + BorrowerStateCheckedOut: {BorrowerActionCheckIn}, + BorrowerStateCheckedIn: {BorrowerActionShipReturn}, +} +var returnableLenderStateActionMapping = map[pr_db.PatronRequestState][]pr_db.PatronRequestAction{ + LenderStateNew: {LenderActionValidate}, + LenderStateValidated: {LenderActionWillSupply, LenderActionCannotSupply, LenderActionAddCondition}, + LenderStateWillSupply: {LenderActionAddCondition, LenderActionCannotSupply, LenderActionShip}, + LenderStateConditionPending: {LenderActionCannotSupply}, + LenderStateConditionAccepted: {LenderActionShip, LenderActionCannotSupply}, + LenderStateShippedReturn: {LenderActionMarkReceived}, + LenderStateCancelRequested: {LenderActionMarkCancelled, LenderActionWillSupply}, +} + +type ActionMappingService struct { +} + +func (r *ActionMappingService) GetActionMapping(pr pr_db.PatronRequest) ActionMapping { + return new(ReturnableActionMapping) +} + +type ReturnableActionMapping struct { +} + +func (r *ReturnableActionMapping) IsActionAvailable(pr pr_db.PatronRequest, action pr_db.PatronRequestAction) bool { + if pr.Side == SideBorrowing { + return isActionAvailable(pr.State, action, returnableBorrowerStateActionMapping) + } else { + return isActionAvailable(pr.State, action, returnableLenderStateActionMapping) + } +} +func (r *ReturnableActionMapping) GetActionsForPatronRequest(pr pr_db.PatronRequest) []pr_db.PatronRequestAction { + if pr.Side == SideBorrowing { + return getActionsByStateFromMapping(pr.State, returnableBorrowerStateActionMapping) + } else { + return getActionsByStateFromMapping(pr.State, returnableLenderStateActionMapping) + } +} + +func isActionAvailable(state pr_db.PatronRequestState, action pr_db.PatronRequestAction, actionMapping map[pr_db.PatronRequestState][]pr_db.PatronRequestAction) bool { + return slices.Contains(getActionsByStateFromMapping(state, actionMapping), action) +} +func getActionsByStateFromMapping(state pr_db.PatronRequestState, actionMapping map[pr_db.PatronRequestState][]pr_db.PatronRequestAction) []pr_db.PatronRequestAction { + if actions, ok := actionMapping[state]; ok { + return actions + } else { + return []pr_db.PatronRequestAction{} + } +} diff --git a/broker/patron_request/service/action_mapping_test.go b/broker/patron_request/service/action_mapping_test.go new file mode 100644 index 00000000..838bfe32 --- /dev/null +++ b/broker/patron_request/service/action_mapping_test.go @@ -0,0 +1,29 @@ +package prservice + +import ( + pr_db "github.com/indexdata/crosslink/broker/patron_request/db" + "github.com/stretchr/testify/assert" + "testing" +) + +var actionMappingService = ActionMappingService{} + +func TestIsActionAvailable(t *testing.T) { + // Borrower + assert.True(t, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).IsActionAvailable(pr_db.PatronRequest{Side: SideBorrowing, State: BorrowerStateNew}, BorrowerActionValidate)) + assert.False(t, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).IsActionAvailable(pr_db.PatronRequest{Side: SideBorrowing, State: BorrowerStateNew}, BorrowerActionReceive)) + + // Lender + assert.True(t, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).IsActionAvailable(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply}, LenderActionShip)) + assert.False(t, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).IsActionAvailable(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply}, BorrowerActionRejectCondition)) +} + +func TestGetActionsForPatronRequest(t *testing.T) { + // Borrower + assert.Equal(t, []pr_db.PatronRequestAction{BorrowerActionValidate}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideBorrowing, State: BorrowerStateNew})) + assert.Equal(t, []pr_db.PatronRequestAction{}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideBorrowing, State: BorrowerStateCompleted})) + + // Lender + assert.Equal(t, []pr_db.PatronRequestAction{LenderActionAddCondition, LenderActionCannotSupply, LenderActionShip}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply})) + assert.Equal(t, []pr_db.PatronRequestAction{}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateShipped})) +} diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index 3cb868e4..3155dbc3 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -4,35 +4,25 @@ import ( "context" "encoding/xml" "errors" - "net/http" - "strings" - "testing" - "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/handler" "github.com/indexdata/crosslink/broker/ill_db" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" - "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "net/http" + "strings" + "testing" ) var appCtx = common.CreateExtCtxWithArgs(context.Background(), nil) var patronRequestId = "pr1" -func TestGetBorrowerActionsByState(t *testing.T) { - assert.Equal(t, []string{ActionValidate}, GetBorrowerActionsByState(BorrowerStateNew)) - assert.Equal(t, []string{}, GetBorrowerActionsByState(BorrowerStateCompleted)) -} +var actionValidate = BorrowerActionValidate -func TestIsBorrowerActionAvailable(t *testing.T) { - assert.True(t, IsBorrowerActionAvailable(BorrowerStateNew, ActionValidate)) - assert.False(t, IsBorrowerActionAvailable(BorrowerStateNew, ActionCheckOut)) - assert.False(t, IsBorrowerActionAvailable(BorrowerStateCompleted, ActionValidate)) -} func TestInvokeAction(t *testing.T) { mockEventBus := new(MockEventBus) prAction := CreatePatronRequestActionService(*new(pr_db.PrRepo), *new(ill_db.IllRepo), mockEventBus, new(handler.Iso18626Handler)) @@ -60,18 +50,29 @@ func TestHandleInvokeActionNoPR(t *testing.T) { prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, errors.New("not fund")) - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionValidate}}}) + 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, "failed to read patron request", resultData.EventError.Message) } +func TestHandleInvokeActionNoPRSide(t *testing.T) { + mockPrRepo := new(MockPrRepo) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateNew, Side: "helper"}, 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, "side helper is not supported", resultData.EventError.Message) +} + func TestHandleInvokeActionWhichIsNotAllowed(t *testing.T) { mockPrRepo := new(MockPrRepo) prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing}, nil) - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionValidate}}}) + 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, "state VALIDATED does not support action validate", resultData.EventError.Message) @@ -80,9 +81,9 @@ func TestHandleInvokeActionWhichIsNotAllowed(t *testing.T) { func TestHandleInvokeActionValidate(t *testing.T) { mockPrRepo := new(MockPrRepo) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateNew, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionValidate}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Nil(t, resultData) @@ -93,22 +94,37 @@ func TestHandleInvokeActionSendRequest(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: getDbText("ISIL:REC1"), IllRequest: []byte("{}")}, nil) + action := BorrowerActionSendRequest - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionSendRequest}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resultData.IncomingMessage.RequestConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateSent, mockPrRepo.savedPr.State) } +func TestHandleInvokeActionSendRequestError(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionSendRequest + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to parse request", resultData.EventError.Message) +} + func TestHandleInvokeActionReceive(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateShipped, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionReceive - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionReceive}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resultData.IncomingMessage.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) @@ -118,9 +134,10 @@ func TestHandleInvokeActionReceive(t *testing.T) { func TestHandleInvokeActionCheckOut(t *testing.T) { mockPrRepo := new(MockPrRepo) prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateReceived}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateReceived, Side: SideBorrowing}, nil) + action := BorrowerActionCheckOut - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionCheckOut}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Nil(t, resultData) @@ -130,9 +147,10 @@ func TestHandleInvokeActionCheckOut(t *testing.T) { func TestHandleInvokeActionCheckIn(t *testing.T) { mockPrRepo := new(MockPrRepo) prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedOut}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedOut, Side: SideBorrowing}, nil) + action := BorrowerActionCheckIn - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionCheckIn}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Nil(t, resultData) @@ -143,9 +161,10 @@ func TestHandleInvokeActionShipReturn(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedIn, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionShipReturn - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionShipReturn}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resultData.IncomingMessage.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) @@ -156,9 +175,10 @@ func TestHandleInvokeActionCancelRequest(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateWillSupply, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateWillSupply, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionCancelRequest - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionCancelRequest}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resultData.IncomingMessage.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) @@ -169,9 +189,10 @@ func TestHandleInvokeActionAcceptCondition(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateConditionPending, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateConditionPending, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionAcceptCondition - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionAcceptCondition}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Nil(t, resultData) @@ -182,9 +203,10 @@ func TestHandleInvokeActionRejectCondition(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateConditionPending, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateConditionPending, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionRejectCondition - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &ActionRejectCondition}}}) + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) assert.Nil(t, resultData) @@ -196,7 +218,7 @@ func TestSendBorrowingRequestInvalidSymbol(t *testing.T) { mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - status, resultData := prAction.sendBorrowingRequest(appCtx, pr_db.PatronRequest{State: BorrowerStateValidated, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}}) + status, resultData := prAction.sendBorrowingRequest(appCtx, pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "invalid requester symbol", resultData.EventError.Message) @@ -207,7 +229,7 @@ func TestSendBorrowingRequestMissingSymbol(t *testing.T) { mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - status, resultData := prAction.sendBorrowingRequest(appCtx, pr_db.PatronRequest{State: BorrowerStateValidated}) + status, resultData := prAction.sendBorrowingRequest(appCtx, pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "missing requester symbol", resultData.EventError.Message) @@ -218,7 +240,7 @@ func TestShipReturnBorrowingRequestMissingSupplierSymbol(t *testing.T) { mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}) + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "missing supplier symbol", resultData.EventError.Message) @@ -229,7 +251,7 @@ func TestShipReturnBorrowingRequestMissingRequesterSymbol(t *testing.T) { mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "missing requester symbol", resultData.EventError.Message) @@ -240,7 +262,7 @@ func TestShipReturnBorrowingRequestInvalidSupplierSymbol(t *testing.T) { mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "x"}}) + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "x"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "invalid supplier symbol", resultData.EventError.Message) @@ -251,12 +273,127 @@ func TestShipReturnBorrowingRequestInvalidRequesterSymbol(t *testing.T) { mockIso18626Handler := new(MockIso18626Handler) prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "invalid requester symbol", resultData.EventError.Message) } +func TestHandleInvokeLenderActionValidate(t *testing.T) { + mockPrRepo := new(MockPrRepo) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateNew, Side: SideLending}, nil) + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateValidated, mockPrRepo.savedPr.State) +} +func TestHandleInvokeLenderActionWillSupply(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionWillSupply + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateWillSupply, mockPrRepo.savedPr.State) +} +func TestHandleInvokeLenderActionCannotSupply(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionCannotSupply + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateUnfilled, mockPrRepo.savedPr.State) +} +func TestHandleInvokeLenderActionAddCondition(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionAddCondition + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateConditionPending, mockPrRepo.savedPr.State) +} +func TestHandleInvokeLenderActionShip(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateWillSupply, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionShip + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateShipped, mockPrRepo.savedPr.State) +} +func TestHandleInvokeLenderActionMarkReceived(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateShippedReturn, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionMarkReceived + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateCompleted, mockPrRepo.savedPr.State) +} +func TestHandleInvokeLenderActionMarkCancelled(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateCancelRequested, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionMarkCancelled + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusSuccess, status) + assert.Nil(t, resultData) + assert.Equal(t, LenderStateCancelled, mockPrRepo.savedPr.State) +} + +func TestHandleInvokeLenderActionMarkCancelledMissingSupplierSymbol(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateCancelRequested, Side: SideLending, SupplierSymbol: pgtype.Text{Valid: false, String: ""}, RequesterSymbol: getDbText("ISIL:REQ1")}, nil) + action := LenderActionMarkCancelled + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "missing supplier symbol", resultData.EventError.Message) +} + +func TestHandleInvokeLenderActionMarkCancelledMissingRequesterSymbol(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateCancelRequested, Side: SideLending, RequesterSymbol: pgtype.Text{Valid: false, String: ""}, SupplierSymbol: getDbText("ISIL:SUP1")}, nil) + action := LenderActionMarkCancelled + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "missing requester symbol", resultData.EventError.Message) +} + type MockEventBus struct { mock.Mock events.EventBus @@ -293,13 +430,18 @@ func (r *MockPrRepo) GetPatronRequestById(ctx common.ExtendedContext, id string) } func (r *MockPrRepo) SavePatronRequest(ctx common.ExtendedContext, params pr_db.SavePatronRequestParams) (pr_db.PatronRequest, error) { - if strings.Contains(params.ID, "error") { + if strings.Contains(params.ID, "error") || strings.Contains(params.RequesterReqID.String, "error") { return pr_db.PatronRequest{}, errors.New("db error") } r.savedPr = pr_db.PatronRequest(params) return pr_db.PatronRequest(params), nil } +func (r *MockPrRepo) GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx common.ExtendedContext, symbol string, requesterReqId string) (pr_db.PatronRequest, error) { + args := r.Called(symbol, requesterReqId) + return args.Get(0).(pr_db.PatronRequest), args.Error(1) +} + type MockIso18626Handler struct { mock.Mock handler.Iso18626Handler @@ -347,6 +489,27 @@ func (h *MockIso18626Handler) HandleRequestingAgencyMessage(ctx common.ExtendedC w.WriteHeader(http.StatusOK) w.Write(output) } +func (h *MockIso18626Handler) HandleSupplyingAgencyMessage(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter) { + status := iso18626.TypeMessageStatusOK + if illMessage.SupplyingAgencyMessage.Header.RequestingAgencyRequestId == "error" { + status = iso18626.TypeMessageStatusERROR + } + var resmsg = &iso18626.ISO18626Message{ + SupplyingAgencyMessageConfirmation: &iso18626.SupplyingAgencyMessageConfirmation{ + ConfirmationHeader: iso18626.ConfirmationHeader{ + MessageStatus: status, + }, + }, + } + output, err := xml.MarshalIndent(resmsg, " ", " ") + if err != nil { + ctx.Logger().Error("failed to produce response", "error", err, "body", string(output)) + return + } + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusOK) + w.Write(output) +} type MockIllRepo struct { mock.Mock diff --git a/broker/patron_request/service/message-handler.go b/broker/patron_request/service/message-handler.go index c3a76e2c..0158eb4e 100644 --- a/broker/patron_request/service/message-handler.go +++ b/broker/patron_request/service/message-handler.go @@ -1,48 +1,55 @@ package prservice import ( + "encoding/json" "errors" - "strings" - + "fmt" + "github.com/google/uuid" "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/ill_db" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" + "github.com/indexdata/go-utils/utils" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + "strings" + "time" ) +var SUPPLIER_PATRON_PATTERN = utils.GetEnv("SUPPLIER_PATRON_PATTERN", "%v_user") + const COMP_MESSAGE = "pr_massage_handler" const RESHARE_ADD_LOAN_CONDITION = "#ReShareAddLoanCondition#" type PatronRequestMessageHandler struct { prRepo pr_db.PrRepo eventRepo events.EventRepo - illRep ill_db.IllRepo + illRepo ill_db.IllRepo eventBus events.EventBus } -func CreatePatronRequestMessageHandler(prRepo pr_db.PrRepo, eventRepo events.EventRepo, illRep ill_db.IllRepo, eventBus events.EventBus) PatronRequestMessageHandler { +func CreatePatronRequestMessageHandler(prRepo pr_db.PrRepo, eventRepo events.EventRepo, illRepo ill_db.IllRepo, eventBus events.EventBus) PatronRequestMessageHandler { return PatronRequestMessageHandler{ prRepo: prRepo, eventRepo: eventRepo, - illRep: illRep, + illRepo: illRepo, eventBus: eventBus, } } func (m *PatronRequestMessageHandler) HandleMessage(ctx common.ExtendedContext, msg *iso18626.ISO18626Message) (*iso18626.ISO18626Message, error) { + ctx = ctx.WithArgs(ctx.LoggerArgs().WithComponent(COMP_MESSAGE)) if msg == nil { return nil, errors.New("cannot process nil message") } - requestId := getPatronRequestId(*msg) - pr, err := m.prRepo.GetPatronRequestById(ctx, requestId) + pr, err := m.getPatronRequest(ctx, *msg) if err != nil { return nil, err } // Create notice with result - status, response, err := m.handlePatronRequestMessage(ctx, msg) + status, response, err := m.handlePatronRequestMessage(ctx, msg, pr) eventData := events.EventData{CommonEventData: events.CommonEventData{IncomingMessage: msg, OutgoingMessage: response}} if err != nil { eventData.EventError = &events.EventError{ @@ -57,38 +64,36 @@ func (m *PatronRequestMessageHandler) HandleMessage(ctx common.ExtendedContext, return response, err } -func (m *PatronRequestMessageHandler) handlePatronRequestMessage(ctx common.ExtendedContext, msg *iso18626.ISO18626Message) (events.EventStatus, *iso18626.ISO18626Message, error) { +func (m *PatronRequestMessageHandler) handlePatronRequestMessage(ctx common.ExtendedContext, msg *iso18626.ISO18626Message, pr pr_db.PatronRequest) (events.EventStatus, *iso18626.ISO18626Message, error) { if msg.SupplyingAgencyMessage != nil { - return m.handleSupplyingAgencyMessage(ctx, *msg.SupplyingAgencyMessage) + return m.handleSupplyingAgencyMessage(ctx, *msg.SupplyingAgencyMessage, pr) } else if msg.RequestingAgencyMessage != nil { - return events.EventStatusError, nil, errors.New("requesting agency message handling is not implemented yet") + return m.handleRequestingAgencyMessage(ctx, *msg.RequestingAgencyMessage, pr) } else if msg.Request != nil { - return events.EventStatusError, nil, errors.New("request handling is not implemented yet") + return m.handleRequestMessage(ctx, *msg.Request) } else { return events.EventStatusError, nil, errors.New("cannot process message without content") } } -func getPatronRequestId(msg iso18626.ISO18626Message) string { +func (m *PatronRequestMessageHandler) getPatronRequest(ctx common.ExtendedContext, msg iso18626.ISO18626Message) (pr_db.PatronRequest, error) { if msg.SupplyingAgencyMessage != nil { - return msg.SupplyingAgencyMessage.Header.RequestingAgencyRequestId + return m.prRepo.GetPatronRequestById(ctx, msg.SupplyingAgencyMessage.Header.RequestingAgencyRequestId) } else if msg.RequestingAgencyMessage != nil { - return msg.RequestingAgencyMessage.Header.SupplyingAgencyRequestId + if msg.RequestingAgencyMessage.Header.SupplyingAgencyRequestId != "" { + return m.prRepo.GetPatronRequestById(ctx, msg.RequestingAgencyMessage.Header.SupplyingAgencyRequestId) + } else { + symbol := msg.RequestingAgencyMessage.Header.SupplyingAgencyId.AgencyIdType.Text + ":" + msg.RequestingAgencyMessage.Header.SupplyingAgencyId.AgencyIdValue + return m.prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx, symbol, msg.RequestingAgencyMessage.Header.RequestingAgencyRequestId) + } } else if msg.Request != nil { - return msg.Request.Header.RequestingAgencyRequestId + return m.prRepo.GetPatronRequestById(ctx, msg.Request.Header.RequestingAgencyRequestId) } else { - return "" + return pr_db.PatronRequest{}, errors.New("missing message") } } -func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessage(ctx common.ExtendedContext, sam iso18626.SupplyingAgencyMessage) (events.EventStatus, *iso18626.ISO18626Message, error) { - pr, err := m.prRepo.GetPatronRequestById(ctx, sam.Header.RequestingAgencyRequestId) - if err != nil { - return createSAMResponse(sam, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ - ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, - ErrorValue: "could not find patron request: " + err.Error(), - }, err) - } +func (m *PatronRequestMessageHandler) handleSupplyingAgencyMessage(ctx common.ExtendedContext, sam iso18626.SupplyingAgencyMessage, pr pr_db.PatronRequest) (events.EventStatus, *iso18626.ISO18626Message, error) { // TODO handle notifications switch sam.StatusInfo.Status { case iso18626.TypeStatusExpectToSupply: @@ -157,3 +162,142 @@ func createSAMResponse(sam iso18626.SupplyingAgencyMessage, messageStatus iso186 }, err } + +func createRequestResponse(request iso18626.Request, messageStatus iso18626.TypeMessageStatus, errorData *iso18626.ErrorData, err error) (events.EventStatus, *iso18626.ISO18626Message, error) { + eventStatus := events.EventStatusSuccess + if messageStatus != iso18626.TypeMessageStatusOK { + eventStatus = events.EventStatusProblem + } + return eventStatus, &iso18626.ISO18626Message{ + RequestConfirmation: &iso18626.RequestConfirmation{ + ConfirmationHeader: iso18626.ConfirmationHeader{ + SupplyingAgencyId: &request.Header.SupplyingAgencyId, + RequestingAgencyId: &request.Header.RequestingAgencyId, + RequestingAgencyRequestId: request.Header.RequestingAgencyRequestId, + MessageStatus: messageStatus, + }, + ErrorData: errorData, + }, + }, + err +} + +func (m *PatronRequestMessageHandler) handleRequestMessage(ctx common.ExtendedContext, request iso18626.Request) (events.EventStatus, *iso18626.ISO18626Message, error) { + raRequestId := request.Header.RequestingAgencyRequestId + if raRequestId == "" { + return createRequestResponse(request, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: "missing RequestingAgencyRequestId", + }, nil) + } + supplierSymbol := request.Header.SupplyingAgencyId.AgencyIdType.Text + ":" + request.Header.SupplyingAgencyId.AgencyIdValue + requesterSymbol := request.Header.RequestingAgencyId.AgencyIdType.Text + ":" + request.Header.RequestingAgencyId.AgencyIdValue + _, err := m.prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx, supplierSymbol, raRequestId) + if err != nil { + if !errors.Is(err, pgx.ErrNoRows) { + return createRequestResponse(request, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) + } + } else { + return createRequestResponse(request, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeBadlyFormedMessage, + ErrorValue: "there is already request with this id " + raRequestId, + }, errors.New("duplicate request: there is already a request with this id "+raRequestId)) + } + requestBytes, err := json.Marshal(request) + if err != nil { + return createRequestResponse(request, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) + } + pr, err := m.prRepo.SavePatronRequest(ctx, pr_db.SavePatronRequestParams{ + ID: uuid.NewString(), + Timestamp: pgtype.Timestamp{Valid: true, Time: time.Now()}, + State: LenderStateNew, + Side: SideLending, + Patron: getDbText(fmt.Sprintf(SUPPLIER_PATRON_PATTERN, request.Header.SupplyingAgencyId.AgencyIdValue)), + RequesterSymbol: getDbText(requesterSymbol), + IllRequest: requestBytes, + SupplierSymbol: getDbText(supplierSymbol), + RequesterReqID: getDbText(raRequestId), + }) + if err != nil { + return createRequestResponse(request, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) + } + action := LenderActionValidate + _, err = m.eventBus.CreateTask(pr.ID, events.EventNameInvokeAction, events.EventData{CommonEventData: events.CommonEventData{Action: &action}}, events.EventDomainPatronRequest, nil) + if err != nil { + return createRequestResponse(request, iso18626.TypeMessageStatusERROR, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) + } + + return createRequestResponse(request, iso18626.TypeMessageStatusOK, nil, nil) +} + +func getDbText(value string) pgtype.Text { + return pgtype.Text{ + Valid: true, + String: value, + } +} + +func (m *PatronRequestMessageHandler) handleRequestingAgencyMessage(ctx common.ExtendedContext, ram iso18626.RequestingAgencyMessage, pr pr_db.PatronRequest) (events.EventStatus, *iso18626.ISO18626Message, error) { + switch ram.Action { + case iso18626.TypeActionNotification, + iso18626.TypeActionStatusRequest, + iso18626.TypeActionRenew, + iso18626.TypeActionShippedForward, + iso18626.TypeActionReceived: + return m.updatePatronRequestAndCreateRamResponse(ctx, pr, ram, &ram.Action) + case iso18626.TypeActionCancel: + pr.State = LenderStateCancelRequested + return m.updatePatronRequestAndCreateRamResponse(ctx, pr, ram, &ram.Action) + case iso18626.TypeActionShippedReturn: + pr.State = LenderStateShippedReturn + return m.updatePatronRequestAndCreateRamResponse(ctx, pr, ram, &ram.Action) + } + err := errors.New("unknown action: " + string(ram.Action)) + return createRAMResponse(ram, iso18626.TypeMessageStatusERROR, &ram.Action, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) +} + +func createRAMResponse(ram iso18626.RequestingAgencyMessage, messageStatus iso18626.TypeMessageStatus, action *iso18626.TypeAction, errorData *iso18626.ErrorData, err error) (events.EventStatus, *iso18626.ISO18626Message, error) { + eventStatus := events.EventStatusSuccess + if messageStatus != iso18626.TypeMessageStatusOK { + eventStatus = events.EventStatusProblem + } + return eventStatus, &iso18626.ISO18626Message{ + RequestingAgencyMessageConfirmation: &iso18626.RequestingAgencyMessageConfirmation{ + ConfirmationHeader: iso18626.ConfirmationHeader{ + SupplyingAgencyId: &ram.Header.SupplyingAgencyId, + RequestingAgencyId: &ram.Header.RequestingAgencyId, + RequestingAgencyRequestId: ram.Header.RequestingAgencyRequestId, + MessageStatus: messageStatus, + }, + Action: action, + ErrorData: errorData, + }, + }, + err +} + +func (m *PatronRequestMessageHandler) updatePatronRequestAndCreateRamResponse(ctx common.ExtendedContext, pr pr_db.PatronRequest, ram iso18626.RequestingAgencyMessage, action *iso18626.TypeAction) (events.EventStatus, *iso18626.ISO18626Message, error) { + _, err := m.prRepo.SavePatronRequest(ctx, pr_db.SavePatronRequestParams(pr)) + if err != nil { + return createRAMResponse(ram, iso18626.TypeMessageStatusERROR, action, &iso18626.ErrorData{ + ErrorType: iso18626.TypeErrorTypeUnrecognisedDataValue, + ErrorValue: err.Error(), + }, err) + } + return createRAMResponse(ram, iso18626.TypeMessageStatusOK, action, nil, nil) +} diff --git a/broker/patron_request/service/message-handler_test.go b/broker/patron_request/service/message-handler_test.go index df11b4a4..bfe6d3fa 100644 --- a/broker/patron_request/service/message-handler_test.go +++ b/broker/patron_request/service/message-handler_test.go @@ -2,16 +2,22 @@ package prservice import ( "errors" - "testing" - "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/ill_db" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" + "github.com/jackc/pgx/v5" "github.com/stretchr/testify/assert" + "testing" ) -func TestGetPatronRequestId(t *testing.T) { +func TestGetPatronRequest(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockPrRepo.On("GetPatronRequestById", "req-id-1").Return(pr_db.PatronRequest{ID: "req-id-1"}, nil) + mockPrRepo.On("GetPatronRequestById", "sam-id-1").Return(pr_db.PatronRequest{ID: "sam-id-1"}, nil) + mockPrRepo.On("GetPatronRequestBySupplierSymbolAndRequesterReqId", "ISIL:SUP1", "req-id-1").Return(pr_db.PatronRequest{ID: "sam-id-1"}, nil) + + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) msg := iso18626.ISO18626Message{ Request: &iso18626.Request{ Header: iso18626.Header{ @@ -20,27 +26,56 @@ func TestGetPatronRequestId(t *testing.T) { }, }, } - assert.Equal(t, "req-id-1", getPatronRequestId(msg)) + pr, err := handler.getPatronRequest(appCtx, msg) + assert.NoError(t, err) + assert.Equal(t, "req-id-1", pr.ID) msg = iso18626.ISO18626Message{ RequestingAgencyMessage: &iso18626.RequestingAgencyMessage{ Header: iso18626.Header{ - RequestingAgencyRequestId: "ram-id-1", + RequestingAgencyRequestId: "req-id-1", SupplyingAgencyRequestId: "sam-id-1", }, }, } - assert.Equal(t, "sam-id-1", getPatronRequestId(msg)) + pr, err = handler.getPatronRequest(appCtx, msg) + assert.NoError(t, err) + assert.Equal(t, "sam-id-1", pr.ID) + + msg = iso18626.ISO18626Message{ + RequestingAgencyMessage: &iso18626.RequestingAgencyMessage{ + Header: iso18626.Header{ + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "SUP1", + }, + RequestingAgencyRequestId: "req-id-1", + SupplyingAgencyRequestId: "", + }, + }, + } + pr, err = handler.getPatronRequest(appCtx, msg) + assert.NoError(t, err) + assert.Equal(t, "sam-id-1", pr.ID) msg = iso18626.ISO18626Message{ SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{ Header: iso18626.Header{ - RequestingAgencyRequestId: "ram-id-1", + RequestingAgencyRequestId: "req-id-1", SupplyingAgencyRequestId: "sam-id-1", }, }, } - assert.Equal(t, "ram-id-1", getPatronRequestId(msg)) + pr, err = handler.getPatronRequest(appCtx, msg) + assert.NoError(t, err) + assert.Equal(t, "req-id-1", pr.ID) + + msg = iso18626.ISO18626Message{} + pr, err = handler.getPatronRequest(appCtx, msg) + assert.Equal(t, "missing message", err.Error()) + assert.Equal(t, "", pr.ID) } func TestHandleMessageNoMessage(t *testing.T) { @@ -93,53 +128,30 @@ func TestHandlePatronRequestMessage(t *testing.T) { mockPrRepo := new(MockPrRepo) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) - status, resp, err := handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{}) + status, resp, err := handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{}, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusError, status) assert.Nil(t, resp) assert.Equal(t, "cannot process message without content", err.Error()) - status, resp, err = handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{Request: &iso18626.Request{}}) - assert.Equal(t, events.EventStatusError, status) - assert.Nil(t, resp) - assert.Equal(t, "request handling is not implemented yet", err.Error()) + status, resp, err = handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{Request: &iso18626.Request{}}, pr_db.PatronRequest{}) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, "missing RequestingAgencyRequestId", resp.RequestConfirmation.ErrorData.ErrorValue) + assert.Nil(t, err) - status, resp, err = handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{RequestingAgencyMessage: &iso18626.RequestingAgencyMessage{}}) - assert.Equal(t, events.EventStatusError, status) - assert.Nil(t, resp) - assert.Equal(t, "requesting agency message handling is not implemented yet", err.Error()) + status, resp, err = handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{RequestingAgencyMessage: &iso18626.RequestingAgencyMessage{}}, pr_db.PatronRequest{}) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, "unknown action: ", resp.RequestingAgencyMessageConfirmation.ErrorData.ErrorValue) + assert.Equal(t, "unknown action: ", err.Error()) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, errors.New("db error")) - status, resp, err = handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{Header: iso18626.Header{RequestingAgencyRequestId: patronRequestId}}}) + status, resp, err = handler.handlePatronRequestMessage(appCtx, &iso18626.ISO18626Message{SupplyingAgencyMessage: &iso18626.SupplyingAgencyMessage{Header: iso18626.Header{RequestingAgencyRequestId: patronRequestId}}}, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusProblem, status) - assert.Equal(t, "db error", err.Error()) - assert.Equal(t, "could not find patron request: db error", resp.SupplyingAgencyMessageConfirmation.ErrorData.ErrorValue) -} - -func TestHandleSupplyingAgencyMessageNoSupplier(t *testing.T) { - mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) - mockIllRepo := new(MockIllRepo) - mockIllRepo.On("GetPeerBySymbol", "ISIL:SUP1").Return(ill_db.Peer{}, errors.New("db error")) - handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), mockIllRepo, *new(events.EventBus)) - - status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ - Header: iso18626.Header{ - SupplyingAgencyId: iso18626.TypeAgencyId{ - AgencyIdType: iso18626.TypeSchemeValuePair{Text: "ISIL"}, - AgencyIdValue: "SUP1", - }, - RequestingAgencyRequestId: patronRequestId, - }, - StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusExpectToSupply}, - }) - assert.NoError(t, err) - assert.Equal(t, events.EventStatusSuccess, status) - assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "status change no allowed", err.Error()) + assert.Equal(t, "status change no allowed", resp.SupplyingAgencyMessageConfirmation.ErrorData.ErrorValue) } func TestHandleSupplyingAgencyMessageExpectToSupply(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -151,7 +163,7 @@ func TestHandleSupplyingAgencyMessageExpectToSupply(t *testing.T) { RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusExpectToSupply}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.NoError(t, err) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) @@ -160,7 +172,6 @@ func TestHandleSupplyingAgencyMessageExpectToSupply(t *testing.T) { func TestHandleSupplyingAgencyMessageWillSupply(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -168,7 +179,7 @@ func TestHandleSupplyingAgencyMessageWillSupply(t *testing.T) { RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusWillSupply}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateWillSupply, mockPrRepo.savedPr.State) @@ -177,7 +188,6 @@ func TestHandleSupplyingAgencyMessageWillSupply(t *testing.T) { func TestHandleSupplyingAgencyMessageWillSupplyCondition(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -188,7 +198,7 @@ func TestHandleSupplyingAgencyMessageWillSupplyCondition(t *testing.T) { MessageInfo: iso18626.MessageInfo{ Note: RESHARE_ADD_LOAN_CONDITION + " some comment", }, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateConditionPending, mockPrRepo.savedPr.State) @@ -197,7 +207,6 @@ func TestHandleSupplyingAgencyMessageWillSupplyCondition(t *testing.T) { func TestHandleSupplyingAgencyMessageLoaned(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -205,7 +214,7 @@ func TestHandleSupplyingAgencyMessageLoaned(t *testing.T) { RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusLoaned}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateShipped, mockPrRepo.savedPr.State) @@ -214,7 +223,6 @@ func TestHandleSupplyingAgencyMessageLoaned(t *testing.T) { func TestHandleSupplyingAgencyMessageLoanCompleted(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -222,7 +230,7 @@ func TestHandleSupplyingAgencyMessageLoanCompleted(t *testing.T) { RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusLoanCompleted}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateCompleted, mockPrRepo.savedPr.State) @@ -231,7 +239,6 @@ func TestHandleSupplyingAgencyMessageLoanCompleted(t *testing.T) { func TestHandleSupplyingAgencyMessageUnfilled(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -239,7 +246,7 @@ func TestHandleSupplyingAgencyMessageUnfilled(t *testing.T) { RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusUnfilled}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateUnfilled, mockPrRepo.savedPr.State) @@ -248,7 +255,6 @@ func TestHandleSupplyingAgencyMessageUnfilled(t *testing.T) { func TestHandleSupplyingAgencyMessageCancelled(t *testing.T) { mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ @@ -256,7 +262,7 @@ func TestHandleSupplyingAgencyMessageCancelled(t *testing.T) { RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateCancelled, mockPrRepo.savedPr.State) @@ -264,18 +270,252 @@ func TestHandleSupplyingAgencyMessageCancelled(t *testing.T) { } func TestHandleSupplyingAgencyMessageNoImplemented(t *testing.T) { - mockPrRepo := new(MockPrRepo) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, nil) - handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + handler := CreatePatronRequestMessageHandler(new(MockPrRepo), *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ Header: iso18626.Header{ RequestingAgencyRequestId: patronRequestId, }, StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusEmpty}, - }) + }, pr_db.PatronRequest{}) assert.Equal(t, events.EventStatusProblem, status) assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, "status change no allowed", resp.SupplyingAgencyMessageConfirmation.ErrorData.ErrorValue) assert.Equal(t, "status change no allowed", err.Error()) } + +func TestHandleSupplyingAgencyMessageCancelledFailToSave(t *testing.T) { + handler := CreatePatronRequestMessageHandler(new(MockPrRepo), *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + + status, resp, err := handler.handleSupplyingAgencyMessage(appCtx, iso18626.SupplyingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyRequestId: patronRequestId, + }, + StatusInfo: iso18626.StatusInfo{Status: iso18626.TypeStatusCancelled}, + }, pr_db.PatronRequest{ID: "error"}) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.SupplyingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "db error", err.Error()) +} + +func TestHandleRequestingAgencyMessageNotification(t *testing.T) { + mockPrRepo := new(MockPrRepo) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + + status, resp, err := handler.handleRequestingAgencyMessage(appCtx, iso18626.RequestingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyRequestId: patronRequestId, + }, + Action: iso18626.TypeActionNotification, + }, pr_db.PatronRequest{State: LenderStateWillSupply}) + assert.Equal(t, events.EventStatusSuccess, status) + assert.Equal(t, iso18626.TypeMessageStatusOK, resp.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, LenderStateWillSupply, mockPrRepo.savedPr.State) + assert.NoError(t, err) +} + +func TestHandleRequestingAgencyMessageCancel(t *testing.T) { + mockPrRepo := new(MockPrRepo) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + + status, resp, err := handler.handleRequestingAgencyMessage(appCtx, iso18626.RequestingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyRequestId: patronRequestId, + }, + Action: iso18626.TypeActionCancel, + }, pr_db.PatronRequest{State: LenderStateWillSupply}) + assert.Equal(t, events.EventStatusSuccess, status) + assert.Equal(t, iso18626.TypeMessageStatusOK, resp.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, LenderStateCancelRequested, mockPrRepo.savedPr.State) + assert.NoError(t, err) +} + +func TestHandleRequestingAgencyMessageShippedReturn(t *testing.T) { + mockPrRepo := new(MockPrRepo) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + + status, resp, err := handler.handleRequestingAgencyMessage(appCtx, iso18626.RequestingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyRequestId: patronRequestId, + }, + Action: iso18626.TypeActionShippedReturn, + }, pr_db.PatronRequest{State: LenderStateWillSupply}) + assert.Equal(t, events.EventStatusSuccess, status) + assert.Equal(t, iso18626.TypeMessageStatusOK, resp.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, LenderStateShippedReturn, mockPrRepo.savedPr.State) + assert.NoError(t, err) +} + +func TestHandleRequestingAgencyMessageUnknown(t *testing.T) { + mockPrRepo := new(MockPrRepo) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + + status, resp, err := handler.handleRequestingAgencyMessage(appCtx, iso18626.RequestingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyRequestId: patronRequestId, + }, + Action: "unknown", + }, pr_db.PatronRequest{State: LenderStateWillSupply}) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "unknown action: unknown", err.Error()) +} + +func TestHandleRequestingAgencyMessageFailToSave(t *testing.T) { + handler := CreatePatronRequestMessageHandler(new(MockPrRepo), *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) + + status, resp, err := handler.handleRequestingAgencyMessage(appCtx, iso18626.RequestingAgencyMessage{ + Header: iso18626.Header{ + RequestingAgencyRequestId: patronRequestId, + }, + Action: iso18626.TypeActionShippedReturn, + }, pr_db.PatronRequest{State: LenderStateWillSupply, ID: "error"}) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "db error", err.Error()) +} + +func TestHandleRequestMessage(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockPrRepo.On("GetPatronRequestBySupplierSymbolAndRequesterReqId", "ISIL:SUP1", "req-id-1").Return(pr_db.PatronRequest{}, pgx.ErrNoRows) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), mockEventBus) + + status, resp, err := handler.handleRequestMessage(appCtx, iso18626.Request{ + Header: iso18626.Header{ + RequestingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "REQ1", + }, + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "SUP1", + }, + RequestingAgencyRequestId: "req-id-1", + }, + }) + assert.Equal(t, events.EventStatusSuccess, status) + assert.Equal(t, iso18626.TypeMessageStatusOK, resp.RequestConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, LenderStateNew, mockPrRepo.savedPr.State) + assert.NoError(t, err) +} + +func TestHandleRequestMessageMissingRequestId(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockPrRepo.On("GetPatronRequestBySupplierSymbolAndRequesterReqId", "ISIL:SUP1", "req-id-1").Return(pr_db.PatronRequest{}, pgx.ErrNoRows) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), mockEventBus) + + status, resp, err := handler.handleRequestMessage(appCtx, iso18626.Request{ + Header: iso18626.Header{ + RequestingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "REQ1", + }, + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "SUP1", + }, + RequestingAgencyRequestId: "", + }, + }) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.RequestConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "missing RequestingAgencyRequestId", resp.RequestConfirmation.ErrorData.ErrorValue) + assert.NoError(t, err) +} + +func TestHandleRequestMessageExistingRequest(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockPrRepo.On("GetPatronRequestBySupplierSymbolAndRequesterReqId", "ISIL:SUP1", "req-id-1").Return(pr_db.PatronRequest{}, nil) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), mockEventBus) + + status, resp, err := handler.handleRequestMessage(appCtx, iso18626.Request{ + Header: iso18626.Header{ + RequestingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "REQ1", + }, + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "SUP1", + }, + RequestingAgencyRequestId: "req-id-1", + }, + }) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.RequestConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "there is already request with this id req-id-1", resp.RequestConfirmation.ErrorData.ErrorValue) + assert.Equal(t, "duplicate request: there is already a request with this id req-id-1", err.Error()) +} + +func TestHandleRequestMessageSearchDbError(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockPrRepo.On("GetPatronRequestBySupplierSymbolAndRequesterReqId", "ISIL:SUP1", "req-id-1").Return(pr_db.PatronRequest{}, errors.New("db error")) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), mockEventBus) + + status, resp, err := handler.handleRequestMessage(appCtx, iso18626.Request{ + Header: iso18626.Header{ + RequestingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "REQ1", + }, + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "SUP1", + }, + RequestingAgencyRequestId: "req-id-1", + }, + }) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.RequestConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "db error", resp.RequestConfirmation.ErrorData.ErrorValue) + assert.Equal(t, "db error", err.Error()) +} + +func TestHandleRequestMessageSaveError(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockEventBus := new(MockEventBus) + mockPrRepo.On("GetPatronRequestBySupplierSymbolAndRequesterReqId", "ISIL:SUP1", "error").Return(pr_db.PatronRequest{}, pgx.ErrNoRows) + handler := CreatePatronRequestMessageHandler(mockPrRepo, *new(events.EventRepo), *new(ill_db.IllRepo), mockEventBus) + + status, resp, err := handler.handleRequestMessage(appCtx, iso18626.Request{ + Header: iso18626.Header{ + RequestingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "REQ1", + }, + SupplyingAgencyId: iso18626.TypeAgencyId{ + AgencyIdType: iso18626.TypeSchemeValuePair{ + Text: "ISIL", + }, + AgencyIdValue: "SUP1", + }, + RequestingAgencyRequestId: "error", + }, + }) + assert.Equal(t, events.EventStatusProblem, status) + assert.Equal(t, iso18626.TypeMessageStatusERROR, resp.RequestConfirmation.ConfirmationHeader.MessageStatus) + assert.Equal(t, "db error", resp.RequestConfirmation.ErrorData.ErrorValue) + assert.Equal(t, "db error", err.Error()) +} diff --git a/broker/sqlc/pr_query.sql b/broker/sqlc/pr_query.sql index 2c349d3e..7c6b261b 100644 --- a/broker/sqlc/pr_query.sql +++ b/broker/sqlc/pr_query.sql @@ -10,8 +10,8 @@ FROM patron_request ORDER BY timestamp; -- name: SavePatronRequest :one -INSERT INTO patron_request (id, timestamp, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +INSERT INTO patron_request (id, timestamp, ill_request, state, side, patron, requester_symbol, supplier_symbol, tenant, requester_req_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (id) DO UPDATE SET timestamp = EXCLUDED.timestamp, ill_request = EXCLUDED.ill_request, @@ -20,10 +20,18 @@ ON CONFLICT (id) DO UPDATE patron = EXCLUDED.patron, requester_symbol = EXCLUDED.requester_symbol, supplier_symbol = EXCLUDED.supplier_symbol, - tenant = EXCLUDED.tenant + tenant = EXCLUDED.tenant, + requester_req_id = EXCLUDED.requester_req_id RETURNING sqlc.embed(patron_request); -- name: DeletePatronRequest :exec DELETE FROM patron_request -WHERE id = $1; \ No newline at end of file +WHERE id = $1; + +-- name: GetPatronRequestBySupplierSymbolAndRequesterReqId :one +-- params: supplier_symbol string, requester_req_id string +SELECT sqlc.embed(patron_request) +FROM patron_request +WHERE supplier_symbol = $1 AND requester_req_id = $2 +LIMIT 1; \ No newline at end of file diff --git a/broker/sqlc/pr_schema.sql b/broker/sqlc/pr_schema.sql index 229672d7..191fa4c2 100644 --- a/broker/sqlc/pr_schema.sql +++ b/broker/sqlc/pr_schema.sql @@ -8,5 +8,6 @@ CREATE TABLE patron_request patron VARCHAR, requester_symbol VARCHAR, supplier_symbol VARCHAR, - tenant VARCHAR + tenant VARCHAR, + requester_req_id VARCHAR ); \ No newline at end of file diff --git a/broker/sqlc/sqlc.yaml b/broker/sqlc/sqlc.yaml index 10b1baa2..1c783193 100644 --- a/broker/sqlc/sqlc.yaml +++ b/broker/sqlc/sqlc.yaml @@ -68,3 +68,10 @@ sql: output_files_suffix: "_gen" sql_package: "pgx/v5" emit_methods_with_db_argument: true + overrides: + - column: "patron_request.state" + go_type: + type: "PatronRequestState" + - column: "patron_request.side" + go_type: + type: "PatronRequestSide" diff --git a/broker/test/api/api-handler_test.go b/broker/test/api/api-handler_test.go index 1b7eacc5..bd16b6e2 100644 --- a/broker/test/api/api-handler_test.go +++ b/broker/test/api/api-handler_test.go @@ -67,7 +67,7 @@ func TestMain(m *testing.M) { app.HTTP_PORT = utils.Must(test.GetFreePort()) ctx, cancel := context.WithCancel(context.Background()) - eventBus, illRepo, eventRepo = apptest.StartApp(ctx) + eventBus, illRepo, eventRepo, _ = apptest.StartApp(ctx) test.WaitForServiceUp(app.HTTP_PORT) defer cancel() diff --git a/broker/test/apputils/apputils.go b/broker/test/apputils/apputils.go index 64719cbc..29348f3a 100644 --- a/broker/test/apputils/apputils.go +++ b/broker/test/apputils/apputils.go @@ -3,6 +3,7 @@ package apputils import ( "context" "fmt" + pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "os" "strconv" "strings" @@ -22,14 +23,14 @@ import ( const EventRecordFormat = "%v, %v = %v" -func StartApp(ctx context.Context) (events.EventBus, ill_db.IllRepo, events.EventRepo) { +func StartApp(ctx context.Context) (events.EventBus, ill_db.IllRepo, events.EventRepo, pr_db.PrRepo) { context, err := app.Init(ctx) utils.Expect(err, "failed to init app") go func() { err := app.StartServer(context) utils.Expect(err, "failed to start server") }() - return context.EventBus, context.IllRepo, context.EventRepo + return context.EventBus, context.IllRepo, context.EventRepo, context.PrRepo } func CreatePgText(value string) pgtype.Text { diff --git a/broker/test/client/client_test.go b/broker/test/client/client_test.go index fa87b439..617b2e2c 100644 --- a/broker/test/client/client_test.go +++ b/broker/test/client/client_test.go @@ -68,7 +68,7 @@ func TestMain(m *testing.M) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - eventBus, illRepo, eventRepo = apptest.StartApp(ctx) + eventBus, illRepo, eventRepo, _ = apptest.StartApp(ctx) test.WaitForServiceUp(app.HTTP_PORT) code := m.Run() diff --git a/broker/test/handler/iso18626-handler_test.go b/broker/test/handler/iso18626-handler_test.go index d546b6c7..dbccc597 100644 --- a/broker/test/handler/iso18626-handler_test.go +++ b/broker/test/handler/iso18626-handler_test.go @@ -72,7 +72,7 @@ func TestMain(m *testing.M) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - _, illRepo, _ = apptest.StartApp(ctx) + _, illRepo, _, _ = apptest.StartApp(ctx) test.WaitForServiceUp(app.HTTP_PORT) code := m.Run() diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 76c06600..0ad9e6b7 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -4,6 +4,9 @@ import ( "bytes" "context" "encoding/json" + "github.com/indexdata/crosslink/broker/common" + pr_db "github.com/indexdata/crosslink/broker/patron_request/db" + "github.com/indexdata/crosslink/iso18626" "io" "net/http" "os" @@ -28,6 +31,7 @@ import ( var basePath = "/patron_requests" var illRepo ill_db.IllRepo +var prRepo pr_db.PrRepo func TestMain(m *testing.M) { app.TENANT_TO_SYMBOL = "ISIL:DK-{tenant}" @@ -58,7 +62,7 @@ func TestMain(m *testing.M) { apptest.StartMockApp(mockPort) ctx, cancel := context.WithCancel(context.Background()) - _, illRepo, _ = apptest.StartApp(ctx) + _, illRepo, _, prRepo = apptest.StartApp(ctx) test.WaitForServiceUp(app.HTTP_PORT) defer cancel() @@ -79,7 +83,14 @@ func TestCrud(t *testing.T) { // POST patron := "p1" - illMessage := "{\"request\": {}}" + request := iso18626.Request{ + BibliographicInfo: iso18626.BibliographicInfo{ + SupplierUniqueRecordId: "WILLSUPPLY_LOANED", + }, + } + jsonBytes, err := json.Marshal(request) + assert.NoError(t, err) + illMessage := string(jsonBytes) newPr := proapi.CreatePatronRequest{ ID: uuid.NewString(), Timestamp: time.Now(), @@ -99,12 +110,12 @@ func TestCrud(t *testing.T) { assert.Equal(t, newPr.ID, foundPr.ID) assert.True(t, foundPr.State != "") - assert.Equal(t, prservice.SideBorrowing, foundPr.Side) + assert.Equal(t, string(prservice.SideBorrowing), foundPr.Side) assert.Equal(t, newPr.Timestamp.YearDay(), foundPr.Timestamp.YearDay()) assert.Equal(t, *newPr.RequesterSymbol, *foundPr.RequesterSymbol) assert.Equal(t, *newPr.SupplierSymbol, *foundPr.SupplierSymbol) assert.Equal(t, *newPr.Patron, *foundPr.Patron) - assert.Equal(t, *newPr.IllRequest, *foundPr.IllRequest) + assert.NotNil(t, *foundPr.IllRequest) // GET list respBytes = httpRequest(t, "GET", basePath, []byte{}, 200) @@ -162,6 +173,190 @@ func TestCrud(t *testing.T) { //httpRequest(t, "DELETE", thisPrPath, []byte{}, 404) } +func TestActionsToCompleteState(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + requesterSymbol := "localISIL:REQ" + uuid.NewString() + supplierSymbol := "localISIL:SUP" + uuid.NewString() + + reqPeer := apptest.CreatePeer(t, illRepo, requesterSymbol, adapter.MOCK_CLIENT_URL) + assert.NotNil(t, reqPeer) + supPeer := apptest.CreatePeer(t, illRepo, supplierSymbol, adapter.MOCK_CLIENT_URL) + assert.NotNil(t, supPeer) + + // POST + patron := "p1" + request := iso18626.Request{ + BibliographicInfo: iso18626.BibliographicInfo{ + SupplierUniqueRecordId: "return-" + supplierSymbol + "::WILLSUPPLY_LOANED", + }, + } + jsonBytes, err := json.Marshal(request) + assert.NoError(t, err) + illMessage := string(jsonBytes) + newPr := proapi.CreatePatronRequest{ + ID: uuid.NewString(), + Timestamp: time.Now(), + SupplierSymbol: &supplierSymbol, + RequesterSymbol: &requesterSymbol, + Patron: &patron, + IllRequest: &illMessage, + } + newPrBytes, err := json.Marshal(newPr) + assert.NoError(t, err, "failed to marshal patron request") + + respBytes := httpRequest(t, "POST", basePath, newPrBytes, 201) + + var foundPr proapi.PatronRequest + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + + assert.Equal(t, newPr.ID, foundPr.ID) + requesterPrPath := basePath + "/" + newPr.ID + + // Wait till action available + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", requesterPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.BorrowerActionSendRequest)+"\"]\n" + }) + + action := proapi.ExecuteAction{ + Action: string(prservice.BorrowerActionSendRequest), + } + actionBytes, err := json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", requesterPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Find supplier patron request + test.WaitForPredicateToBeTrue(func() bool { + supPr, _ := prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(appCtx, supplierSymbol, newPr.ID) + return supPr.ID != "" + }) + supPr, err := prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(appCtx, supplierSymbol, newPr.ID) + assert.NoError(t, err) + assert.NotNil(t, supPr.ID) + + // Wait for action + supplierPrPath := basePath + "/" + supPr.ID + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", supplierPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.LenderActionWillSupply)+"\"]\n" + }) + + // Will supply + action = proapi.ExecuteAction{ + Action: string(prservice.LenderActionWillSupply), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", supplierPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Wait for action + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", supplierPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.LenderActionShip)+"\"]\n" + }) + + // Ship + action = proapi.ExecuteAction{ + Action: string(prservice.LenderActionShip), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", supplierPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Wait for action + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", requesterPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.BorrowerActionReceive)+"\"]\n" + }) + + // Receive + action = proapi.ExecuteAction{ + Action: string(prservice.BorrowerActionReceive), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", requesterPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Wait for action + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", requesterPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.BorrowerActionCheckOut)+"\"]\n" + }) + + // Check out + action = proapi.ExecuteAction{ + Action: string(prservice.BorrowerActionCheckOut), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", requesterPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Wait for action + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", requesterPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.BorrowerActionCheckIn)+"\"]\n" + }) + + // Check in + action = proapi.ExecuteAction{ + Action: string(prservice.BorrowerActionCheckIn), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", requesterPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Wait for action + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", requesterPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.BorrowerActionShipReturn)+"\"]\n" + }) + + // Ship return + action = proapi.ExecuteAction{ + Action: string(prservice.BorrowerActionShipReturn), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", requesterPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Wait for action + test.WaitForPredicateToBeTrue(func() bool { + respBytes = httpRequest(t, "GET", supplierPrPath+"/actions", []byte{}, 200) + return string(respBytes) == "[\""+string(prservice.LenderActionMarkReceived)+"\"]\n" + }) + + // Ship return + action = proapi.ExecuteAction{ + Action: string(prservice.LenderActionMarkReceived), + } + actionBytes, err = json.Marshal(action) + assert.NoError(t, err, "failed to marshal patron request action") + respBytes = httpRequest(t, "POST", supplierPrPath+"/action", actionBytes, 200) + assert.Equal(t, "{\"actionResult\":\"SUCCESS\"}\n", string(respBytes)) + + // Check requester patron request done + respBytes = httpRequest(t, "GET", requesterPrPath, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, newPr.ID, foundPr.ID) + assert.Equal(t, string(prservice.BorrowerStateCompleted), foundPr.State) + + // Check supplier patron request done + respBytes = httpRequest(t, "GET", supplierPrPath, []byte{}, 200) + err = json.Unmarshal(respBytes, &foundPr) + assert.NoError(t, err, "failed to unmarshal patron request") + assert.Equal(t, supPr.ID, foundPr.ID) + assert.Equal(t, string(prservice.LenderStateCompleted), foundPr.State) +} + func httpRequest(t *testing.T, method string, uriPath string, reqbytes []byte, expectStatus int) []byte { client := http.DefaultClient hreq, err := http.NewRequest(method, getLocalhostWithPort()+uriPath, bytes.NewBuffer(reqbytes)) diff --git a/broker/test/service/e2e_test.go b/broker/test/service/e2e_test.go index 097698ec..9c23f9a4 100644 --- a/broker/test/service/e2e_test.go +++ b/broker/test/service/e2e_test.go @@ -58,7 +58,7 @@ func TestMain(m *testing.M) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - eventBus, illRepo, eventRepo = apptest.StartApp(ctx) + eventBus, illRepo, eventRepo, _ = apptest.StartApp(ctx) test.WaitForServiceUp(app.HTTP_PORT) code := m.Run() diff --git a/go.work.sum b/go.work.sum index c8ae28e3..f69299b2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -558,6 +558,7 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= @@ -1089,6 +1090,7 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMe golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=