Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions broker/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ broker/**/*_test.go

# Include migrations
!broker/migrations/**

# Include statemodels
!broker/patron_request/service/statemodels
2 changes: 1 addition & 1 deletion broker/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func Init(ctx context.Context) (Context, error) {

sseBroker := api.NewSseBroker(appCtx, common.NewTenant(TENANT_TO_SYMBOL))

AddDefaultHandlers(eventBus, iso18626Client, supplierLocator, workflowManager, iso18626Handler, prActionService, prApiHandler, sseBroker)
AddDefaultHandlers(eventBus, iso18626Client, supplierLocator, workflowManager, iso18626Handler, *prActionService, prApiHandler, sseBroker)
err = StartEventBus(ctx, eventBus)
if err != nil {
return Context{}, err
Expand Down
127 changes: 127 additions & 0 deletions broker/oapi/open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,106 @@ components:
- event
- data

StateModel:
type: object
description: ReShare state model definition
additionalProperties: false
properties:
type:
type: string
description: Self-referential type
name:
type: string
description: Name of the state model
desc:
type: string
description: Description of the state model
version:
type: string
description: Version of the state model in SemVer
states:
type: array
description: A list of all allowed states
items:
$ref: '#/components/schemas/ModelState'

ModelState:
title: State
type: object
description: Definition of a particular state
properties:
name:
type: string
description: Name of the state, in capital letters with underscores
display:
type: string
description: Human-readable state name
desc:
type: string
description: Description of the state, e.g., what situation is modelled by the state
side:
type: string
description: Indicates which side of the request the state belongs to
enum:
- REQUESTER
- SUPPLIER
actions:
type: array
description: List of all actions that may be performed on the request when in this state
items:
$ref: '#/components/schemas/ModelAction'
events:
type: array
description: List of all events that may be triggered to the request when in this state
items:
$ref: '#/components/schemas/ModelEvent'
terminal:
type: boolean
description: Indicates if the state is terminal (meaning no actions or events are allowed in this state)
oneOf:
- anyOf:
- required:
- actions
- required:
- events
- required:
- terminal
required:
- name
- side

ModelAction:
type: object
title: Action
additionalProperties: false
properties:
name:
type: string
description: Description of the action
transitions:
type: array
description: List of all possible state transitions after performing the action. When no transitions are defined, the action is considered to be non-state-changing
items:
type: string
required:
- name

ModelEvent:
type: object
title: Event
additionalProperties: false
properties:
name:
type: string
description: Description of the event
transitions:
type: array
description: List of all possible state transitions after performing the event. When no transitions are defined, the event is considered to be non-state-changing
items:
type: string
required:
- name

paths:
/:
get:
Expand Down Expand Up @@ -1084,3 +1184,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/SseResult'

/state_model/models/{model}:
get:
summary: Retrieve a state model by name
tags:
- patron-requests-api
parameters:
- in: path
name: model
schema:
type: string
required: true
description: The name of the statemodel to retrieve
- $ref: '#/components/parameters/Tenant'
responses:
'200':
description: Successful retrieval of state model
content:
application/json:
schema:
$ref: '#/components/schemas/StateModel'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
10 changes: 10 additions & 0 deletions broker/patron_request/api/api-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ func NewPrApiHandler(prRepo pr_db.PrRepo, eventBus events.EventBus, tenant commo
}
}

func (a *PatronRequestApiHandler) GetStateModelModelsModel(w http.ResponseWriter, r *http.Request, model string, params proapi.GetStateModelModelsModelParams) {
stateModel := a.actionMappingService.SMService.GetStateModel(model)

if stateModel == nil {
addNotFoundError(w)
return
}
writeJsonResponse(w, *stateModel)
}

func (a *PatronRequestApiHandler) GetPatronRequests(w http.ResponseWriter, r *http.Request, params proapi.GetPatronRequestsParams) {
symbol, err := api.GetSymbolForRequest(r, a.tenant, params.XOkapiTenant, params.Symbol)
logParams := map[string]string{"method": "GetPatronRequests", "side": params.Side, "symbol": symbol}
Expand Down
4 changes: 2 additions & 2 deletions broker/patron_request/service/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ type PatronRequestActionService struct {
actionMappingService ActionMappingService
}

func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) PatronRequestActionService {
return PatronRequestActionService{
func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) *PatronRequestActionService {
return &PatronRequestActionService{
prRepo: prRepo,
eventBus: eventBus,
iso18626Handler: iso18626Handler,
Expand Down
87 changes: 55 additions & 32 deletions broker/patron_request/service/action_mapping.go
Original file line number Diff line number Diff line change
@@ -1,64 +1,87 @@
package prservice

import (
pr_db "github.com/indexdata/crosslink/broker/patron_request/db"
"slices"

pr_db "github.com/indexdata/crosslink/broker/patron_request/db"
"github.com/indexdata/crosslink/broker/patron_request/proapi"
)

type ActionMapping interface {
IsActionAvailable(pr pr_db.PatronRequest, action pr_db.PatronRequestAction) bool
GetActionsForPatronRequest(pr pr_db.PatronRequest) []pr_db.PatronRequestAction
type ActionMappingService struct {
SMService StateModelService
}

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},
func (r *ActionMappingService) NewActionMappingService() *ActionMappingService {
return &ActionMappingService{
SMService: StateModelService{},
}
}
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},

func (r *ActionMappingService) GetActionMapping(pr pr_db.PatronRequest) *ActionMapping {
//TODO: check the PatronRequest loan type to decide what kind of state model/mapping to return
return NewActionMapping(r.SMService.GetStateModel("returnables"))
}

type ActionMappingService struct {
type ActionMapping struct {
borrowerStateActionMapping map[pr_db.PatronRequestState][]pr_db.PatronRequestAction
lenderStateActionMapping map[pr_db.PatronRequestState][]pr_db.PatronRequestAction
}

// Constructor function to initialize the mappings for given StateModel
func NewActionMapping(stateModel *proapi.StateModel) *ActionMapping {
r := new(ActionMapping)

borrowerMap := make(map[pr_db.PatronRequestState][]pr_db.PatronRequestAction)
lenderMap := make(map[pr_db.PatronRequestState][]pr_db.PatronRequestAction)

for _, state := range *stateModel.States {
if state.Actions != nil {
nameList := make([]pr_db.PatronRequestAction, 0)
for _, action := range *state.Actions {
nameList = append(nameList, pr_db.PatronRequestAction(action.Name))
}
if state.Side == proapi.REQUESTER {
borrowerMap[pr_db.PatronRequestState(state.Name)] = nameList
} else {
lenderMap[pr_db.PatronRequestState(state.Name)] = nameList
}
}
}

r.borrowerStateActionMapping = borrowerMap
r.lenderStateActionMapping = lenderMap

return r
}

func (r *ActionMappingService) GetActionMapping(pr pr_db.PatronRequest) ActionMapping {
return new(ReturnableActionMapping)
func (r *ActionMapping) GetBorrowerActionsMap() map[pr_db.PatronRequestState][]pr_db.PatronRequestAction {
return r.borrowerStateActionMapping
}

type ReturnableActionMapping struct {
func (r *ActionMapping) GetLenderActionsMap() map[pr_db.PatronRequestState][]pr_db.PatronRequestAction {
return r.lenderStateActionMapping
}

func (r *ReturnableActionMapping) IsActionAvailable(pr pr_db.PatronRequest, action pr_db.PatronRequestAction) bool {
func (r *ActionMapping) IsActionAvailable(pr pr_db.PatronRequest, action pr_db.PatronRequestAction) bool {
if pr.Side == SideBorrowing {
return isActionAvailable(pr.State, action, returnableBorrowerStateActionMapping)
return isActionAvailable(pr.State, action, r.borrowerStateActionMapping)
} else {
return isActionAvailable(pr.State, action, returnableLenderStateActionMapping)
return isActionAvailable(pr.State, action, r.lenderStateActionMapping)
}
}
func (r *ReturnableActionMapping) GetActionsForPatronRequest(pr pr_db.PatronRequest) []pr_db.PatronRequestAction {

func (r *ActionMapping) GetActionsForPatronRequest(pr pr_db.PatronRequest) []pr_db.PatronRequestAction {
if pr.Side == SideBorrowing {
return getActionsByStateFromMapping(pr.State, returnableBorrowerStateActionMapping)
return getActionsByStateFromMapping(pr.State, r.borrowerStateActionMapping)
} else {
return getActionsByStateFromMapping(pr.State, returnableLenderStateActionMapping)
return getActionsByStateFromMapping(pr.State, r.lenderStateActionMapping)
}
}

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
Expand Down
64 changes: 59 additions & 5 deletions broker/patron_request/service/action_mapping_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
package prservice

import (
"slices"
"testing"

pr_db "github.com/indexdata/crosslink/broker/patron_request/db"
"github.com/stretchr/testify/assert"
"testing"
)

func TestNewReturnableActionMapping(t *testing.T) {
borrowerStateActionMapping := 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},
}

lenderStateActionMapping := 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},
}

stateModel, err := LoadStateModelByName("returnables")
assert.Nil(t, err)
returnableActionMapping := NewActionMapping(stateModel)

assert.NotNil(t, returnableActionMapping)

mapCompare(t, returnableActionMapping.borrowerStateActionMapping, borrowerStateActionMapping)

mapCompare(t, returnableActionMapping.lenderStateActionMapping, lenderStateActionMapping)
}

var actionMappingService = ActionMappingService{}

func TestIsActionAvailable(t *testing.T) {
Expand All @@ -20,10 +56,28 @@ func TestIsActionAvailable(t *testing.T) {

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}))
listCompare(t, []pr_db.PatronRequestAction{BorrowerActionValidate}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideBorrowing, State: BorrowerStateNew}))
listCompare(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}))
listCompare(t, []pr_db.PatronRequestAction{LenderActionAddCondition, LenderActionCannotSupply, LenderActionShip}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateWillSupply}))
listCompare(t, []pr_db.PatronRequestAction{}, actionMappingService.GetActionMapping(pr_db.PatronRequest{}).GetActionsForPatronRequest(pr_db.PatronRequest{Side: SideLending, State: LenderStateShipped}))
}

func listCompare(t *testing.T, list1 []pr_db.PatronRequestAction, list2 []pr_db.PatronRequestAction) {
assert.Equal(t, len(list1), len(list2))
for i := range list1 {
assert.True(t, slices.Contains(list2, list1[i]))
}
}

func mapCompare(t *testing.T, map1 map[pr_db.PatronRequestState][]pr_db.PatronRequestAction, map2 map[pr_db.PatronRequestState][]pr_db.PatronRequestAction) {
for stateName := range map1 {
listOne := map1[stateName]
listTwo := map2[stateName]
assert.Equal(t, len(listOne), len(listTwo))
for i := range listOne {
assert.True(t, slices.Contains(listTwo, listOne[i]))
}
}
}
6 changes: 6 additions & 0 deletions broker/patron_request/service/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,3 +869,9 @@ func TestCallNumberFromIllRequest(t *testing.T) {
callNumber := callNumberFromIllRequest(r)
assert.Equal(t, "QA76.73.G63 D37 2020", callNumber)
}

func TestLoadReturnableStateModel(t *testing.T) {
stateModel, err := LoadStateModelByName("returnables")
assert.Nil(t, err)
assert.NotNil(t, stateModel)
}
Loading
Loading