diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index 432343d4dab..5719621edd6 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -466,6 +466,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation shipment.ReviewShipmentAddressUpdate has not yet been implemented") }) } + if api.QueuesSaveBulkAssignmentDataHandler == nil { + api.QueuesSaveBulkAssignmentDataHandler = queues.SaveBulkAssignmentDataHandlerFunc(func(params queues.SaveBulkAssignmentDataParams) middleware.Responder { + return middleware.NotImplemented("operation queues.SaveBulkAssignmentData has not yet been implemented") + }) + } if api.EvaluationReportsSaveEvaluationReportHandler == nil { api.EvaluationReportsSaveEvaluationReportHandler = evaluation_reports.SaveEvaluationReportHandlerFunc(func(params evaluation_reports.SaveEvaluationReportParams) middleware.Responder { return middleware.NotImplemented("operation evaluation_reports.SaveEvaluationReport has not yet been implemented") diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index a1e4e130a28..f6b0b1c08ce 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -4416,6 +4416,46 @@ func init() { } } }, + "/queues/bulk-assignment/assign": { + "post": { + "description": "Supervisor office users are able to assign moves. This endpoint saves office user assignments to multiple moves.\n", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Assigns one or more moves to one or more office users", + "operationId": "saveBulkAssignmentData", + "parameters": [ + { + "name": "bulkAssignmentSavePayload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BulkAssignmentSavePayload" + } + } + ], + "responses": { + "204": { + "description": "assigned" + }, + "401": { + "$ref": "#/responses/PermissionDenied" + }, + "404": { + "$ref": "#/responses/NotFound" + }, + "500": { + "$ref": "#/responses/ServerError" + } + } + } + }, "/queues/counseling": { "get": { "description": "An office services counselor user will be assigned a transportation office that will determine which moves are displayed in their queue based on the origin duty location. GHC moves will show up here onced they have reached the NEEDS SERVICE COUNSELING status after submission from a customer or created on a customer's behalf.\n", @@ -7300,6 +7340,23 @@ func init() { } } }, + "BulkAssignmentForUser": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "moveAssignments": { + "type": "integer", + "x-omitempty": false + } + } + }, + "BulkAssignmentMoveData": { + "type": "string", + "format": "uuid" + }, "BulkAssignmentMoveID": { "type": "string", "format": "uuid", @@ -7311,6 +7368,33 @@ func init() { "$ref": "#/definitions/BulkAssignmentMoveID" } }, + "BulkAssignmentSavePayload": { + "type": "object", + "properties": { + "moveData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentMoveData" + } + }, + "queueType": { + "description": "A string corresponding to the queue type", + "type": "string", + "enum": [ + "COUNSELING", + "CLOSEOUT", + "TASK_ORDER", + "PAYMENT_REQUEST" + ] + }, + "userData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentForUser" + } + } + } + }, "ClientError": { "type": "object", "required": [ @@ -21536,6 +21620,55 @@ func init() { } } }, + "/queues/bulk-assignment/assign": { + "post": { + "description": "Supervisor office users are able to assign moves. This endpoint saves office user assignments to multiple moves.\n", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Assigns one or more moves to one or more office users", + "operationId": "saveBulkAssignmentData", + "parameters": [ + { + "name": "bulkAssignmentSavePayload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BulkAssignmentSavePayload" + } + } + ], + "responses": { + "204": { + "description": "assigned" + }, + "401": { + "description": "The request was denied", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The requested resource wasn't found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "A server error occurred", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/queues/counseling": { "get": { "description": "An office services counselor user will be assigned a transportation office that will determine which moves are displayed in their queue based on the origin duty location. GHC moves will show up here onced they have reached the NEEDS SERVICE COUNSELING status after submission from a customer or created on a customer's behalf.\n", @@ -24850,6 +24983,23 @@ func init() { } } }, + "BulkAssignmentForUser": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "moveAssignments": { + "type": "integer", + "x-omitempty": false + } + } + }, + "BulkAssignmentMoveData": { + "type": "string", + "format": "uuid" + }, "BulkAssignmentMoveID": { "type": "string", "format": "uuid", @@ -24861,6 +25011,33 @@ func init() { "$ref": "#/definitions/BulkAssignmentMoveID" } }, + "BulkAssignmentSavePayload": { + "type": "object", + "properties": { + "moveData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentMoveData" + } + }, + "queueType": { + "description": "A string corresponding to the queue type", + "type": "string", + "enum": [ + "COUNSELING", + "CLOSEOUT", + "TASK_ORDER", + "PAYMENT_REQUEST" + ] + }, + "userData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentForUser" + } + } + } + }, "ClientError": { "type": "object", "required": [ diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index ee86793406f..3a513ab2705 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -309,6 +309,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { ShipmentReviewShipmentAddressUpdateHandler: shipment.ReviewShipmentAddressUpdateHandlerFunc(func(params shipment.ReviewShipmentAddressUpdateParams) middleware.Responder { return middleware.NotImplemented("operation shipment.ReviewShipmentAddressUpdate has not yet been implemented") }), + QueuesSaveBulkAssignmentDataHandler: queues.SaveBulkAssignmentDataHandlerFunc(func(params queues.SaveBulkAssignmentDataParams) middleware.Responder { + return middleware.NotImplemented("operation queues.SaveBulkAssignmentData has not yet been implemented") + }), EvaluationReportsSaveEvaluationReportHandler: evaluation_reports.SaveEvaluationReportHandlerFunc(func(params evaluation_reports.SaveEvaluationReportParams) middleware.Responder { return middleware.NotImplemented("operation evaluation_reports.SaveEvaluationReport has not yet been implemented") }), @@ -616,6 +619,8 @@ type MymoveAPI struct { ShipmentRequestShipmentReweighHandler shipment.RequestShipmentReweighHandler // ShipmentReviewShipmentAddressUpdateHandler sets the operation handler for the review shipment address update operation ShipmentReviewShipmentAddressUpdateHandler shipment.ReviewShipmentAddressUpdateHandler + // QueuesSaveBulkAssignmentDataHandler sets the operation handler for the save bulk assignment data operation + QueuesSaveBulkAssignmentDataHandler queues.SaveBulkAssignmentDataHandler // EvaluationReportsSaveEvaluationReportHandler sets the operation handler for the save evaluation report operation EvaluationReportsSaveEvaluationReportHandler evaluation_reports.SaveEvaluationReportHandler // CustomerSearchCustomersHandler sets the operation handler for the search customers operation @@ -1004,6 +1009,9 @@ func (o *MymoveAPI) Validate() error { if o.ShipmentReviewShipmentAddressUpdateHandler == nil { unregistered = append(unregistered, "shipment.ReviewShipmentAddressUpdateHandler") } + if o.QueuesSaveBulkAssignmentDataHandler == nil { + unregistered = append(unregistered, "queues.SaveBulkAssignmentDataHandler") + } if o.EvaluationReportsSaveEvaluationReportHandler == nil { unregistered = append(unregistered, "evaluation_reports.SaveEvaluationReportHandler") } @@ -1512,6 +1520,10 @@ func (o *MymoveAPI) initHandlerCache() { o.handlers["PATCH"] = make(map[string]http.Handler) } o.handlers["PATCH"]["/shipments/{shipmentID}/review-shipment-address-update"] = shipment.NewReviewShipmentAddressUpdate(o.context, o.ShipmentReviewShipmentAddressUpdateHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/queues/bulk-assignment/assign"] = queues.NewSaveBulkAssignmentData(o.context, o.QueuesSaveBulkAssignmentDataHandler) if o.handlers["PUT"] == nil { o.handlers["PUT"] = make(map[string]http.Handler) } diff --git a/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data.go new file mode 100644 index 00000000000..32537ec144c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// SaveBulkAssignmentDataHandlerFunc turns a function with the right signature into a save bulk assignment data handler +type SaveBulkAssignmentDataHandlerFunc func(SaveBulkAssignmentDataParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn SaveBulkAssignmentDataHandlerFunc) Handle(params SaveBulkAssignmentDataParams) middleware.Responder { + return fn(params) +} + +// SaveBulkAssignmentDataHandler interface for that can handle valid save bulk assignment data params +type SaveBulkAssignmentDataHandler interface { + Handle(SaveBulkAssignmentDataParams) middleware.Responder +} + +// NewSaveBulkAssignmentData creates a new http.Handler for the save bulk assignment data operation +func NewSaveBulkAssignmentData(ctx *middleware.Context, handler SaveBulkAssignmentDataHandler) *SaveBulkAssignmentData { + return &SaveBulkAssignmentData{Context: ctx, Handler: handler} +} + +/* + SaveBulkAssignmentData swagger:route POST /queues/bulk-assignment/assign queues saveBulkAssignmentData + +# Assigns one or more moves to one or more office users + +Supervisor office users are able to assign moves. This endpoint saves office user assignments to multiple moves. +*/ +type SaveBulkAssignmentData struct { + Context *middleware.Context + Handler SaveBulkAssignmentDataHandler +} + +func (o *SaveBulkAssignmentData) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewSaveBulkAssignmentDataParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_parameters.go new file mode 100644 index 00000000000..87d3a9ca0f8 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_parameters.go @@ -0,0 +1,84 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/validate" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// NewSaveBulkAssignmentDataParams creates a new SaveBulkAssignmentDataParams object +// +// There are no default values defined in the spec. +func NewSaveBulkAssignmentDataParams() SaveBulkAssignmentDataParams { + + return SaveBulkAssignmentDataParams{} +} + +// SaveBulkAssignmentDataParams contains all the bound params for the save bulk assignment data operation +// typically these are obtained from a http.Request +// +// swagger:parameters saveBulkAssignmentData +type SaveBulkAssignmentDataParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + BulkAssignmentSavePayload *ghcmessages.BulkAssignmentSavePayload +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewSaveBulkAssignmentDataParams() beforehand. +func (o *SaveBulkAssignmentDataParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body ghcmessages.BulkAssignmentSavePayload + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("bulkAssignmentSavePayload", "body", "")) + } else { + res = append(res, errors.NewParseError("bulkAssignmentSavePayload", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.BulkAssignmentSavePayload = &body + } + } + } else { + res = append(res, errors.Required("bulkAssignmentSavePayload", "body", "")) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_responses.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_responses.go new file mode 100644 index 00000000000..99a269fdfda --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_responses.go @@ -0,0 +1,174 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// SaveBulkAssignmentDataNoContentCode is the HTTP code returned for type SaveBulkAssignmentDataNoContent +const SaveBulkAssignmentDataNoContentCode int = 204 + +/* +SaveBulkAssignmentDataNoContent assigned + +swagger:response saveBulkAssignmentDataNoContent +*/ +type SaveBulkAssignmentDataNoContent struct { +} + +// NewSaveBulkAssignmentDataNoContent creates SaveBulkAssignmentDataNoContent with default headers values +func NewSaveBulkAssignmentDataNoContent() *SaveBulkAssignmentDataNoContent { + + return &SaveBulkAssignmentDataNoContent{} +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(204) +} + +// SaveBulkAssignmentDataUnauthorizedCode is the HTTP code returned for type SaveBulkAssignmentDataUnauthorized +const SaveBulkAssignmentDataUnauthorizedCode int = 401 + +/* +SaveBulkAssignmentDataUnauthorized The request was denied + +swagger:response saveBulkAssignmentDataUnauthorized +*/ +type SaveBulkAssignmentDataUnauthorized struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewSaveBulkAssignmentDataUnauthorized creates SaveBulkAssignmentDataUnauthorized with default headers values +func NewSaveBulkAssignmentDataUnauthorized() *SaveBulkAssignmentDataUnauthorized { + + return &SaveBulkAssignmentDataUnauthorized{} +} + +// WithPayload adds the payload to the save bulk assignment data unauthorized response +func (o *SaveBulkAssignmentDataUnauthorized) WithPayload(payload *ghcmessages.Error) *SaveBulkAssignmentDataUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the save bulk assignment data unauthorized response +func (o *SaveBulkAssignmentDataUnauthorized) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(401) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// SaveBulkAssignmentDataNotFoundCode is the HTTP code returned for type SaveBulkAssignmentDataNotFound +const SaveBulkAssignmentDataNotFoundCode int = 404 + +/* +SaveBulkAssignmentDataNotFound The requested resource wasn't found + +swagger:response saveBulkAssignmentDataNotFound +*/ +type SaveBulkAssignmentDataNotFound struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewSaveBulkAssignmentDataNotFound creates SaveBulkAssignmentDataNotFound with default headers values +func NewSaveBulkAssignmentDataNotFound() *SaveBulkAssignmentDataNotFound { + + return &SaveBulkAssignmentDataNotFound{} +} + +// WithPayload adds the payload to the save bulk assignment data not found response +func (o *SaveBulkAssignmentDataNotFound) WithPayload(payload *ghcmessages.Error) *SaveBulkAssignmentDataNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the save bulk assignment data not found response +func (o *SaveBulkAssignmentDataNotFound) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// SaveBulkAssignmentDataInternalServerErrorCode is the HTTP code returned for type SaveBulkAssignmentDataInternalServerError +const SaveBulkAssignmentDataInternalServerErrorCode int = 500 + +/* +SaveBulkAssignmentDataInternalServerError A server error occurred + +swagger:response saveBulkAssignmentDataInternalServerError +*/ +type SaveBulkAssignmentDataInternalServerError struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewSaveBulkAssignmentDataInternalServerError creates SaveBulkAssignmentDataInternalServerError with default headers values +func NewSaveBulkAssignmentDataInternalServerError() *SaveBulkAssignmentDataInternalServerError { + + return &SaveBulkAssignmentDataInternalServerError{} +} + +// WithPayload adds the payload to the save bulk assignment data internal server error response +func (o *SaveBulkAssignmentDataInternalServerError) WithPayload(payload *ghcmessages.Error) *SaveBulkAssignmentDataInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the save bulk assignment data internal server error response +func (o *SaveBulkAssignmentDataInternalServerError) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_urlbuilder.go new file mode 100644 index 00000000000..d021ae6b61c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_urlbuilder.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// SaveBulkAssignmentDataURL generates an URL for the save bulk assignment data operation +type SaveBulkAssignmentDataURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SaveBulkAssignmentDataURL) WithBasePath(bp string) *SaveBulkAssignmentDataURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SaveBulkAssignmentDataURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *SaveBulkAssignmentDataURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/queues/bulk-assignment/assign" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *SaveBulkAssignmentDataURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *SaveBulkAssignmentDataURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *SaveBulkAssignmentDataURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on SaveBulkAssignmentDataURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on SaveBulkAssignmentDataURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *SaveBulkAssignmentDataURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_for_user.go b/pkg/gen/ghcmessages/bulk_assignment_for_user.go new file mode 100644 index 00000000000..7c0c46c1f31 --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_for_user.go @@ -0,0 +1,77 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BulkAssignmentForUser bulk assignment for user +// +// swagger:model BulkAssignmentForUser +type BulkAssignmentForUser struct { + + // id + // Format: uuid + ID strfmt.UUID `json:"id,omitempty"` + + // move assignments + MoveAssignments int64 `json:"moveAssignments"` +} + +// Validate validates this bulk assignment for user +func (m *BulkAssignmentForUser) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentForUser) validateID(formats strfmt.Registry) error { + if swag.IsZero(m.ID) { // not required + return nil + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this bulk assignment for user based on context it is used +func (m *BulkAssignmentForUser) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *BulkAssignmentForUser) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkAssignmentForUser) UnmarshalBinary(b []byte) error { + var res BulkAssignmentForUser + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_move_data.go b/pkg/gen/ghcmessages/bulk_assignment_move_data.go new file mode 100644 index 00000000000..c69c23c9ee7 --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_move_data.go @@ -0,0 +1,38 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// BulkAssignmentMoveData bulk assignment move data +// +// swagger:model BulkAssignmentMoveData +type BulkAssignmentMoveData strfmt.UUID + +// Validate validates this bulk assignment move data +func (m BulkAssignmentMoveData) Validate(formats strfmt.Registry) error { + var res []error + + if err := validate.FormatOf("", "body", "uuid", strfmt.UUID(m).String(), formats); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this bulk assignment move data based on context it is used +func (m BulkAssignmentMoveData) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_save_payload.go b/pkg/gen/ghcmessages/bulk_assignment_save_payload.go new file mode 100644 index 00000000000..376206bdf7c --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_save_payload.go @@ -0,0 +1,233 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BulkAssignmentSavePayload bulk assignment save payload +// +// swagger:model BulkAssignmentSavePayload +type BulkAssignmentSavePayload struct { + + // move data + MoveData []BulkAssignmentMoveData `json:"moveData"` + + // A string corresponding to the queue type + // Enum: [COUNSELING CLOSEOUT TASK_ORDER PAYMENT_REQUEST] + QueueType string `json:"queueType,omitempty"` + + // user data + UserData []*BulkAssignmentForUser `json:"userData"` +} + +// Validate validates this bulk assignment save payload +func (m *BulkAssignmentSavePayload) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateMoveData(formats); err != nil { + res = append(res, err) + } + + if err := m.validateQueueType(formats); err != nil { + res = append(res, err) + } + + if err := m.validateUserData(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentSavePayload) validateMoveData(formats strfmt.Registry) error { + if swag.IsZero(m.MoveData) { // not required + return nil + } + + for i := 0; i < len(m.MoveData); i++ { + + if err := m.MoveData[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("moveData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("moveData" + "." + strconv.Itoa(i)) + } + return err + } + + } + + return nil +} + +var bulkAssignmentSavePayloadTypeQueueTypePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["COUNSELING","CLOSEOUT","TASK_ORDER","PAYMENT_REQUEST"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + bulkAssignmentSavePayloadTypeQueueTypePropEnum = append(bulkAssignmentSavePayloadTypeQueueTypePropEnum, v) + } +} + +const ( + + // BulkAssignmentSavePayloadQueueTypeCOUNSELING captures enum value "COUNSELING" + BulkAssignmentSavePayloadQueueTypeCOUNSELING string = "COUNSELING" + + // BulkAssignmentSavePayloadQueueTypeCLOSEOUT captures enum value "CLOSEOUT" + BulkAssignmentSavePayloadQueueTypeCLOSEOUT string = "CLOSEOUT" + + // BulkAssignmentSavePayloadQueueTypeTASKORDER captures enum value "TASK_ORDER" + BulkAssignmentSavePayloadQueueTypeTASKORDER string = "TASK_ORDER" + + // BulkAssignmentSavePayloadQueueTypePAYMENTREQUEST captures enum value "PAYMENT_REQUEST" + BulkAssignmentSavePayloadQueueTypePAYMENTREQUEST string = "PAYMENT_REQUEST" +) + +// prop value enum +func (m *BulkAssignmentSavePayload) validateQueueTypeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, bulkAssignmentSavePayloadTypeQueueTypePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *BulkAssignmentSavePayload) validateQueueType(formats strfmt.Registry) error { + if swag.IsZero(m.QueueType) { // not required + return nil + } + + // value enum + if err := m.validateQueueTypeEnum("queueType", "body", m.QueueType); err != nil { + return err + } + + return nil +} + +func (m *BulkAssignmentSavePayload) validateUserData(formats strfmt.Registry) error { + if swag.IsZero(m.UserData) { // not required + return nil + } + + for i := 0; i < len(m.UserData); i++ { + if swag.IsZero(m.UserData[i]) { // not required + continue + } + + if m.UserData[i] != nil { + if err := m.UserData[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("userData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("userData" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this bulk assignment save payload based on the context it is used +func (m *BulkAssignmentSavePayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateMoveData(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateUserData(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentSavePayload) contextValidateMoveData(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.MoveData); i++ { + + if swag.IsZero(m.MoveData[i]) { // not required + return nil + } + + if err := m.MoveData[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("moveData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("moveData" + "." + strconv.Itoa(i)) + } + return err + } + + } + + return nil +} + +func (m *BulkAssignmentSavePayload) contextValidateUserData(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.UserData); i++ { + + if m.UserData[i] != nil { + + if swag.IsZero(m.UserData[i]) { // not required + return nil + } + + if err := m.UserData[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("userData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("userData" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *BulkAssignmentSavePayload) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkAssignmentSavePayload) UnmarshalBinary(b []byte) error { + var res BulkAssignmentSavePayload + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index 3bca03020cb..de468ebd102 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -575,6 +575,13 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { move.NewMoveFetcherBulkAssignment(), } + ghcAPI.QueuesSaveBulkAssignmentDataHandler = SaveBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + move.NewMoveFetcher(), + move.NewMoveAssignerBulkAssignment(), + } + ghcAPI.QueuesGetMovesQueueHandler = GetMovesQueueHandler{ handlerConfig, order.NewOrderFetcher(waf), diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index 4d708d415b4..f85628ef8bc 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -816,6 +816,64 @@ func (h GetBulkAssignmentDataHandler) Handle( }) } +// SaveBulkAssignmentDataHandler saves the bulk assignment data +type SaveBulkAssignmentDataHandler struct { + handlers.HandlerConfig + services.OfficeUserFetcherPop + services.MoveFetcher + services.MoveAssigner +} + +func (h SaveBulkAssignmentDataHandler) Handle( + params queues.SaveBulkAssignmentDataParams, +) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + if !appCtx.Session().IsOfficeUser() { + err := apperror.NewForbiddenError("not an office user") + appCtx.Logger().Error("Must be an office user", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataUnauthorized(), err + } + + officeUser, err := h.OfficeUserFetcherPop.FetchOfficeUserByID(appCtx, appCtx.Session().OfficeUserID) + if err != nil { + appCtx.Logger().Error("Error retrieving office_user", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataNotFound(), err + } + + privileges, err := models.FetchPrivilegesForUser(appCtx.DB(), *officeUser.UserID) + if err != nil { + appCtx.Logger().Error("Error retreiving user privileges", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataNotFound(), err + } + + isSupervisor := privileges.HasPrivilege(models.PrivilegeTypeSupervisor) + if !isSupervisor { + appCtx.Logger().Error("Unauthorized", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataUnauthorized(), err + } + + queueType := params.BulkAssignmentSavePayload.QueueType + moveData := params.BulkAssignmentSavePayload.MoveData + userData := params.BulkAssignmentSavePayload.UserData + + // fetch the moves available to be assigned to their office users + movesForAssignment, err := h.MoveFetcher.FetchMovesByIdArray(appCtx, moveData) + if err != nil { + appCtx.Logger().Error("Error retreiving moves for assignment", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataInternalServerError(), err + } + + _, err = h.MoveAssigner.BulkMoveAssignment(appCtx, queueType, userData, movesForAssignment) + if err != nil { + appCtx.Logger().Error("Error assigning moves", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + + return queues.NewSaveBulkAssignmentDataNoContent(), nil + }) +} + // GetServicesCounselingOriginListHandler returns the origin list for the Service Counselor user via GET /queues/counselor/origin-list type GetServicesCounselingOriginListHandler struct { handlers.HandlerConfig diff --git a/pkg/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index d3d39152f8c..386495d13b0 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -2252,7 +2252,138 @@ func (suite *HandlerSuite) TestAvailableOfficeUsers() { suite.Equal(subtestData.officeUsers[0].ID.String(), payload.QueuePaymentRequests[0].AvailableOfficeUsers[0].OfficeUserID.String()) suite.Equal(subtestData.officeUsers[1].ID.String(), payload.QueuePaymentRequests[0].AvailableOfficeUsers[1].OfficeUserID.String()) }) +} + +func (suite *HandlerSuite) TestSaveBulkAssignmentDataHandler() { + suite.Run("returns an unauthorized error when an attempt is made by a non supervisor", func() { + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + }, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 1}, + } + moveData := []ghcmessages.BulkAssignmentMoveData{ghcmessages.BulkAssignmentMoveData(move.ID.String())} + + request := httptest.NewRequest("POST", "/queues/bulk-assignment/assign", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.SaveBulkAssignmentDataParams{ + HTTPRequest: request, + BulkAssignmentSavePayload: &ghcmessages.BulkAssignmentSavePayload{ + QueueType: "COUNSELING", + MoveData: moveData, + UserData: userData, + }, + } + handlerConfig := suite.HandlerConfig() + handler := SaveBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcher(), + movefetcher.NewMoveAssignerBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.SaveBulkAssignmentDataUnauthorized{}, response) + }) + + suite.Run("successfully assigns bulk assignments", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 1}, + } + moveData := []ghcmessages.BulkAssignmentMoveData{ghcmessages.BulkAssignmentMoveData(move.ID.String())} + + request := httptest.NewRequest("POST", "/queues/bulk-assignment/assign", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.SaveBulkAssignmentDataParams{ + HTTPRequest: request, + BulkAssignmentSavePayload: &ghcmessages.BulkAssignmentSavePayload{ + QueueType: "COUNSELING", + MoveData: moveData, + UserData: userData, + }, + } + handlerConfig := suite.HandlerConfig() + handler := SaveBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcher(), + movefetcher.NewMoveAssignerBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.SaveBulkAssignmentDataNoContent{}, response) + }) } func (suite *HandlerSuite) TestGetDestinationRequestsQueuesHandler() { diff --git a/pkg/services/mocks/MoveAssigner.go b/pkg/services/mocks/MoveAssigner.go new file mode 100644 index 00000000000..5aa23ae5697 --- /dev/null +++ b/pkg/services/mocks/MoveAssigner.go @@ -0,0 +1,61 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + appcontext "github.com/transcom/mymove/pkg/appcontext" + ghcmessages "github.com/transcom/mymove/pkg/gen/ghcmessages" + + mock "github.com/stretchr/testify/mock" + + models "github.com/transcom/mymove/pkg/models" +) + +// MoveAssigner is an autogenerated mock type for the MoveAssigner type +type MoveAssigner struct { + mock.Mock +} + +// BulkMoveAssignment provides a mock function with given fields: appCtx, queueType, officeUserData, movesToAssign +func (_m *MoveAssigner) BulkMoveAssignment(appCtx appcontext.AppContext, queueType string, officeUserData []*ghcmessages.BulkAssignmentForUser, movesToAssign models.Moves) (*models.Moves, error) { + ret := _m.Called(appCtx, queueType, officeUserData, movesToAssign) + + if len(ret) == 0 { + panic("no return value specified for BulkMoveAssignment") + } + + var r0 *models.Moves + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, []*ghcmessages.BulkAssignmentForUser, models.Moves) (*models.Moves, error)); ok { + return rf(appCtx, queueType, officeUserData, movesToAssign) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, []*ghcmessages.BulkAssignmentForUser, models.Moves) *models.Moves); ok { + r0 = rf(appCtx, queueType, officeUserData, movesToAssign) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Moves) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, []*ghcmessages.BulkAssignmentForUser, models.Moves) error); ok { + r1 = rf(appCtx, queueType, officeUserData, movesToAssign) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMoveAssigner creates a new instance of MoveAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMoveAssigner(t interface { + mock.TestingT + Cleanup(func()) +}) *MoveAssigner { + mock := &MoveAssigner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/MoveFetcher.go b/pkg/services/mocks/MoveFetcher.go index 22a26abbd68..454d022156b 100644 --- a/pkg/services/mocks/MoveFetcher.go +++ b/pkg/services/mocks/MoveFetcher.go @@ -3,8 +3,10 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" appcontext "github.com/transcom/mymove/pkg/appcontext" + ghcmessages "github.com/transcom/mymove/pkg/gen/ghcmessages" + + mock "github.com/stretchr/testify/mock" models "github.com/transcom/mymove/pkg/models" @@ -46,6 +48,36 @@ func (_m *MoveFetcher) FetchMove(appCtx appcontext.AppContext, locator string, s return r0, r1 } +// FetchMovesByIdArray provides a mock function with given fields: appCtx, moveIds +func (_m *MoveFetcher) FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) { + ret := _m.Called(appCtx, moveIds) + + if len(ret) == 0 { + panic("no return value specified for FetchMovesByIdArray") + } + + var r0 models.Moves + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []ghcmessages.BulkAssignmentMoveData) (models.Moves, error)); ok { + return rf(appCtx, moveIds) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []ghcmessages.BulkAssignmentMoveData) models.Moves); ok { + r0 = rf(appCtx, moveIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.Moves) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, []ghcmessages.BulkAssignmentMoveData) error); ok { + r1 = rf(appCtx, moveIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FetchMovesForPPTASReports provides a mock function with given fields: appCtx, params func (_m *MoveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *services.MoveTaskOrderFetcherParams) (models.Moves, error) { ret := _m.Called(appCtx, params) diff --git a/pkg/services/move.go b/pkg/services/move.go index ff3abe681de..47792c5877b 100644 --- a/pkg/services/move.go +++ b/pkg/services/move.go @@ -8,6 +8,7 @@ import ( "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/gen/ghcmessages" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/storage" @@ -27,6 +28,7 @@ type MoveListFetcher interface { type MoveFetcher interface { FetchMove(appCtx appcontext.AppContext, locator string, searchParams *MoveFetcherParams) (*models.Move, error) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *MoveTaskOrderFetcherParams) (models.Moves, error) + FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) } type MoveFetcherBulkAssignment interface { @@ -136,3 +138,10 @@ type MoveAssignedOfficeUserUpdater interface { type CheckForLockedMovesAndUnlockHandler interface { CheckForLockedMovesAndUnlock(appCtx appcontext.AppContext, officeUserID uuid.UUID) error } + +// MoveAssigner is the exported interface for bulk assigning moves to office users +// +//go:generate mockery --name MoveAssigner +type MoveAssigner interface { + BulkMoveAssignment(appCtx appcontext.AppContext, queueType string, officeUserData []*ghcmessages.BulkAssignmentForUser, movesToAssign models.Moves) (*models.Moves, error) +} diff --git a/pkg/services/move/move_assignment.go b/pkg/services/move/move_assignment.go new file mode 100644 index 00000000000..a55211f9e81 --- /dev/null +++ b/pkg/services/move/move_assignment.go @@ -0,0 +1,62 @@ +package move + +import ( + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/gen/ghcmessages" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" +) + +type moveAssigner struct { +} + +func NewMoveAssignerBulkAssignment() services.MoveAssigner { + return &moveAssigner{} +} + +func (a moveAssigner) BulkMoveAssignment(appCtx appcontext.AppContext, queueType string, officeUserData []*ghcmessages.BulkAssignmentForUser, movesToAssign models.Moves) (*models.Moves, error) { + if len(movesToAssign) == 0 { + return nil, apperror.NewBadDataError("No moves to assign") + } + + transactionErr := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { + for _, move := range movesToAssign { + for _, officeUser := range officeUserData { + if officeUser != nil && officeUser.MoveAssignments > 0 { + officeUserId := uuid.FromStringOrNil(officeUser.ID.String()) + + switch queueType { + case string(models.QueueTypeCounseling): + move.SCAssignedID = &officeUserId + case string(models.QueueTypeCloseout): + move.SCAssignedID = &officeUserId + case string(models.QueueTypeTaskOrder): + move.TOOAssignedID = &officeUserId + case string(models.QueueTypePaymentRequest): + move.TIOAssignedID = &officeUserId + } + + officeUser.MoveAssignments -= 1 + + verrs, err := appCtx.DB().ValidateAndUpdate(&move) + if err != nil || verrs.HasAny() { + return apperror.NewInvalidInputError(move.ID, err, verrs, "") + } + + break + } + } + } + + return nil + }) + + if transactionErr != nil { + return nil, transactionErr + } + + return nil, nil +} diff --git a/pkg/services/move/move_assignment_test.go b/pkg/services/move/move_assignment_test.go new file mode 100644 index 00000000000..5c4ced099af --- /dev/null +++ b/pkg/services/move/move_assignment_test.go @@ -0,0 +1,252 @@ +package move + +import ( + "github.com/go-openapi/strfmt" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/gen/ghcmessages" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" +) + +func (suite *MoveServiceSuite) TestBulkMoveAssignment() { + moveAssigner := NewMoveAssignerBulkAssignment() + + setupTestData := func() (models.TransportationOffice, models.Move, models.Move, models.Move) { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + move1 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + move2 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + move3 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + return transportationOffice, move1, move2, move3 + } + + suite.Run("successfully assigns multiple counseling moves to a SC user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeCounseling), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + + suite.Equal(officeUser.ID, *move1.SCAssignedID) + suite.Equal(officeUser.ID, *move2.SCAssignedID) + suite.Nil(move3.SCAssignedID) + }) + + suite.Run("successfully assigns multiple closeout moves to a SC user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeCloseout), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + + suite.Equal(officeUser.ID, *move1.SCAssignedID) + suite.Equal(officeUser.ID, *move2.SCAssignedID) + suite.Nil(move3.SCAssignedID) + }) + + suite.Run("successfully assigns multiple task order moves to a TOO user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTOO, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeTaskOrder), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + + suite.Equal(officeUser.ID, *move1.TOOAssignedID) + suite.Equal(officeUser.ID, *move2.TOOAssignedID) + suite.Nil(move3.SCAssignedID) + }) + + suite.Run("successfully assigns payment requests to a TIO user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTIO, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypePaymentRequest), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + + suite.Equal(officeUser.ID, *move1.TIOAssignedID) + suite.Equal(officeUser.ID, *move2.TIOAssignedID) + suite.Nil(move3.TIOAssignedID) + }) +} diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index e0d6aea4f8a..e38da9e6000 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -9,6 +9,7 @@ import ( "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/apperror" "github.com/transcom/mymove/pkg/db/utilities" + "github.com/transcom/mymove/pkg/gen/ghcmessages" "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" @@ -62,6 +63,21 @@ func (f moveFetcher) FetchMove(appCtx appcontext.AppContext, locator string, sea return move, nil } +func (f moveFetcher) FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) { + moves := models.Moves{} + + err := appCtx.DB().Q(). + Where("id in (?)", moveIds). + Where("show = TRUE"). + All(&moves) + + if err != nil { + return nil, err + } + + return moves, nil +} + // Fetches moves for Navy servicemembers with approved shipments. Ignores gbloc rules func (f moveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *services.MoveTaskOrderFetcherParams) (models.Moves, error) { var moves models.Moves @@ -172,7 +188,7 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCloseout(appCtx ap query := `SELECT moves.id, - ppm_shipments.submitted_at AS earliest_date + COALESCE(MIN(ppm_shipments.submitted_at), '0001-01-01') AS earliest_date FROM moves INNER JOIN orders ON orders.id = moves.orders_id INNER JOIN service_members ON service_members.id = orders.service_member_id @@ -200,7 +216,7 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCloseout(appCtx ap query += ` AND (ppm_shipments.status IN ($2)) AND (orders.orders_type NOT IN ($3, $4, $5)) - GROUP BY moves.id, ppm_shipments.submitted_at + GROUP BY moves.id ORDER BY earliest_date ASC` err := appCtx.DB().RawQuery(query, @@ -283,7 +299,7 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentPaymentRequest(app sqlQuery := ` SELECT moves.id, - min(payment_requests.requested_at) AS earliest_date + MIN(payment_requests.requested_at) AS earliest_date FROM moves INNER JOIN orders ON orders.id = moves.orders_id INNER JOIN service_members ON orders.service_member_id = service_members.id diff --git a/src/components/BulkAssignment/BulkAssignmentModal.jsx b/src/components/BulkAssignment/BulkAssignmentModal.jsx index 9b84c51b4ce..a1732e44572 100644 --- a/src/components/BulkAssignment/BulkAssignmentModal.jsx +++ b/src/components/BulkAssignment/BulkAssignmentModal.jsx @@ -1,6 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Button } from '@trussworks/react-uswds'; +import { Formik } from 'formik'; +import * as Yup from 'yup'; import styles from './BulkAssignmentModal.module.scss'; @@ -8,16 +10,39 @@ import Modal, { ModalTitle, ModalClose, ModalActions, connectModal } from 'compo import { getBulkAssignmentData } from 'services/ghcApi'; import { milmoveLogger } from 'utils/milmoveLog'; import { userName } from 'utils/formatters'; +import { Form } from 'components/form'; + +const initialValues = { + userData: [], + moveData: [], +}; export const BulkAssignmentModal = ({ onClose, onSubmit, title, submitText, closeText, queueType }) => { - const [showCancelModal, setShowCancelModal] = useState(false); + const [isError, setIsError] = useState(false); const [bulkAssignmentData, setBulkAssignmentData] = useState(null); const [isDisabled, setIsDisabled] = useState(false); const [numberOfMoves, setNumberOfMoves] = useState(0); + const [showCancelModal, setShowCancelModal] = useState(false); + + const errorMessage = 'Cannot assign more moves than are available.'; + + const initUserData = (availableOfficeUsers) => { + const officeUsers = []; + availableOfficeUsers.forEach((user) => { + const newUserAssignment = { + ID: user.officeUserId, + moveAssignments: 0, + }; + officeUsers.push(newUserAssignment); + }); + initialValues.userData = officeUsers; + }; + const fetchData = useCallback(async () => { try { const data = await getBulkAssignmentData(queueType); setBulkAssignmentData(data); + initUserData(data?.availableOfficeUsers); if (!data.bulkAssignmentMoveIDs) { setIsDisabled(true); @@ -34,6 +59,12 @@ export const BulkAssignmentModal = ({ onClose, onSubmit, title, submitText, clos fetchData(); }, [fetchData]); + initialValues.moveData = bulkAssignmentData?.bulkAssignmentMoveIDs; + + const validationSchema = Yup.object().shape({ + assignment: Yup.number().min(0).typeError('Assignment must be a number'), + }); + return (
User | -Workload | -Assignment | -
---|---|---|
- {userName(user)} - |
-
- {user.workload || 0} - |
- - - | -