diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index b995c81b408..5a02296965e 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -12,7 +12,7 @@ import ( "github.com/transcom/mymove/pkg/testdatagen" ) -// BuildOfficeUser creates an OfficeUser +// BuildOfficeUser creates an OfficeUser, and a transportation office and transportation office assignment if either doesn't exist // Params: // - customs is a slice that will be modified by the factory // - db can be set to nil to create a stubbed model that is not stored in DB. @@ -59,6 +59,71 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait mustCreate(db, &officeUser) } + BuildPrimaryTransportationOfficeAssignment(db, []Customization{ + { + Model: models.OfficeUser{ + ID: officeUser.ID, + }, + LinkOnly: true, + }, + { + Model: models.TransportationOffice{ + ID: transportationOffice.ID, + }, + LinkOnly: true, + }, + }, nil) + + return officeUser +} + +// BuildOfficeUserWithoutTransportationAssignment creates an OfficeUser +// Params: +// - customs is a slice that will be modified by the factory +// - db can be set to nil to create a stubbed model that is not stored in DB. +// Notes: +// - To build an office user with one or more roles use BuildOfficeUserWithRoles +// - There's a uniqueness constraint on office user emails so use the GetTraitOfficeUserEmail trait +// when creating a test with multiple office users +// - The OfficeUser returned won't have an ID if the db is nil. If an ID is needed for a stubbed user, +// use trait GetTraitOfficeUserWithID +func BuildOfficeUserWithoutTransportationOfficeAssignment(db *pop.Connection, customs []Customization, traits []Trait) models.OfficeUser { + customs = setupCustomizations(customs, traits) + + // Find officeuser assertion and convert to models officeuser + var cOfficeUser models.OfficeUser + if result := findValidCustomization(customs, OfficeUser); result != nil { + cOfficeUser = result.Model.(models.OfficeUser) + if result.LinkOnly { + return cOfficeUser + } + } + + // Find/create the user model + user := BuildUserAndUsersRoles(db, customs, nil) + + // Find/create the TransportationOffice model + transportationOffice := BuildTransportationOffice(db, customs, nil) + + // create officeuser + officeUser := models.OfficeUser{ + UserID: &user.ID, + User: user, + FirstName: "Leo", + LastName: "Spaceman", + Email: "leo_spaceman_office@example.com", + Telephone: "415-555-1212", + TransportationOffice: transportationOffice, + TransportationOfficeID: transportationOffice.ID, + } + // Overwrite values with those from assertions + testdatagen.MergeModels(&officeUser, cOfficeUser) + + // If db is false, it's a stub. No need to create in database + if db != nil { + mustCreate(db, &officeUser) + } + return officeUser } diff --git a/pkg/factory/shared.go b/pkg/factory/shared.go index 324ab276f7c..a8fc4333d89 100644 --- a/pkg/factory/shared.go +++ b/pkg/factory/shared.go @@ -88,6 +88,7 @@ var SITDurationUpdate CustomType = "SITDurationUpdate" var StorageFacility CustomType = "StorageFacility" var TransportationAccountingCode CustomType = "TransportationAccountingCode" var TransportationOffice CustomType = "TransportationOffice" +var TransportationOfficeAssignment CustomType = "TransportationOfficeAssignment" var Upload CustomType = "Upload" var UserUpload CustomType = "UserUpload" var User CustomType = "User" @@ -148,6 +149,7 @@ var defaultTypesMap = map[string]CustomType{ "models.TransportationAccountingCode": TransportationAccountingCode, "models.UsPostRegionCity": UsPostRegionCity, "models.TransportationOffice": TransportationOffice, + "models.TransportationOfficeAssignment": TransportationOfficeAssignment, "models.Upload": Upload, "models.UserUpload": UserUpload, "models.User": User, diff --git a/pkg/factory/transportation_office_assignment_factory.go b/pkg/factory/transportation_office_assignment_factory.go new file mode 100644 index 00000000000..25651b8bd5c --- /dev/null +++ b/pkg/factory/transportation_office_assignment_factory.go @@ -0,0 +1,94 @@ +package factory + +import ( + "github.com/gobuffalo/pop/v6" + + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" +) + +// BuildPrimaryTransportationOfficeAssignment creates a Transportation Assignment, and a transportation office and officer user if either doesn't exist +// Params: +// - customs is a slice that will be modified by the factory +// - db can be set to nil to create a stubbed model that is not stored in DB. +// Notes: +// - Marks the transportation office assignment as the office user's primary transportation office, +// use BuildAlternateTransportationOfficeAssignment for non-primary transportation office. +func BuildPrimaryTransportationOfficeAssignment(db *pop.Connection, customs []Customization, traits []Trait) models.TransportationOfficeAssignment { + customs = setupCustomizations(customs, traits) + + // Find TransportationAssignment assertion and convert to models.TransportationOfficeAssignment + var cTransportationOfficeAssignment models.TransportationOfficeAssignment + if result := findValidCustomization(customs, TransportationOfficeAssignment); result != nil { + cTransportationOfficeAssignment = result.Model.(models.TransportationOfficeAssignment) + if result.LinkOnly { + return cTransportationOfficeAssignment + } + } + + // Find/Create the associated office user model + officeUser := BuildOfficeUser(db, customs, traits) + + // Find/Create the associated transportation office model + transportationOffice := BuildTransportationOffice(db, customs, traits) + + // Create transportationOffice + transportationOfficeAssignment := models.TransportationOfficeAssignment{ + ID: officeUser.ID, + TransportationOfficeID: transportationOffice.ID, + TransportationOffice: transportationOffice, + PrimaryOffice: true, + } + + // Overwrite values with those from customizations + testdatagen.MergeModels(&transportationOfficeAssignment, cTransportationOfficeAssignment) + + // If db is false, it's a stub. No need to create in database + if db != nil { + mustCreate(db, &transportationOfficeAssignment) + } + return transportationOfficeAssignment +} + +// BuildAlternateTransportationAssignment creates a Transportation Assignment, and a transportation office and officer user if either doesn't exist +// Params: +// - customs is a slice that will be modified by the factory +// - db can be set to nil to create a stubbed model that is not stored in DB. +// Notes: +// - Marks the transportation office assignment as a non-primary transportation office assignment, +// use BuildPrimaryTransportationOfficeAssignment for primary transportation office assignments. +func BuildAlternateTransportationAssignment(db *pop.Connection, customs []Customization, traits []Trait) models.TransportationOfficeAssignment { + customs = setupCustomizations(customs, traits) + + // Find TransportationAssignment assertion and convert to models.TransportationAssignment + var cTransportationOfficeAssignment models.TransportationOfficeAssignment + if result := findValidCustomization(customs, TransportationOfficeAssignment); result != nil { + cTransportationOfficeAssignment = result.Model.(models.TransportationOfficeAssignment) + if result.LinkOnly { + return cTransportationOfficeAssignment + } + } + + // Find/Create the associated office user model + officeUser := BuildOfficeUser(db, customs, traits) + + // Find/Create the associated transportation office model + transportationOffice := BuildTransportationOffice(db, customs, traits) + + // Create transportationOffice + transportationOfficeAssignment := models.TransportationOfficeAssignment{ + ID: officeUser.ID, + TransportationOfficeID: transportationOffice.ID, + TransportationOffice: transportationOffice, + PrimaryOffice: false, + } + + // Overwrite values with those from customizations + testdatagen.MergeModels(&transportationOfficeAssignment, cTransportationOfficeAssignment) + + // If db is false, it's a stub. No need to create in database + if db != nil { + mustCreate(db, &transportationOfficeAssignment) + } + return transportationOfficeAssignment +} diff --git a/pkg/gen/adminapi/embedded_spec.go b/pkg/gen/adminapi/embedded_spec.go index 03274b71847..d53ea28717e 100644 --- a/pkg/gen/adminapi/embedded_spec.go +++ b/pkg/gen/adminapi/embedded_spec.go @@ -2686,6 +2686,7 @@ func init() { "email", "telephone", "transportationOfficeId", + "transportationOfficeAssignments", "active", "roles", "edipi", @@ -2757,6 +2758,12 @@ func init() { "format": "telephone", "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$" }, + "transportationOfficeAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/TransportationOfficeAssignment" + } + }, "transportationOfficeId": { "type": "string", "format": "uuid" @@ -2813,10 +2820,11 @@ func init() { "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", "example": "212-555-5555" }, - "transportationOfficeId": { - "type": "string", - "format": "uuid", - "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + "transportationOfficeAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/OfficeUserTransportationOfficeAssignment" + } } } }, @@ -2854,6 +2862,22 @@ func init() { } } }, + "OfficeUserTransportationOfficeAssignment": { + "type": "object", + "properties": { + "primaryOffice": { + "type": "boolean", + "title": "primaryOffice", + "x-nullable": true + }, + "transportationOfficeId": { + "type": "string", + "format": "uuid", + "title": "transportationOfficeId", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + } + } + }, "OfficeUserUpdate": { "type": "object", "properties": { @@ -2896,10 +2920,11 @@ func init() { "x-nullable": true, "example": "212-555-5555" }, - "transportationOfficeId": { - "type": "string", - "format": "uuid", - "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + "transportationOfficeAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/OfficeUserTransportationOfficeAssignment" + } } } }, @@ -3225,6 +3250,38 @@ func init() { } } }, + "TransportationOfficeAssignment": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "officeUserId": { + "type": "string", + "format": "uuid", + "example": "c56a4780-65aa-42ec-a945-5fd87dec0538" + }, + "primaryOffice": { + "type": "boolean", + "x-omitempty": false + }, + "transportationOffice": { + "$ref": "#/definitions/TransportationOffice" + }, + "transportationOfficeId": { + "type": "string", + "format": "uuid", + "example": "d67a4780-65aa-42ec-a945-5fd87dec0549" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + } + }, "TransportationOffices": { "type": "array", "items": { @@ -6245,6 +6302,7 @@ func init() { "email", "telephone", "transportationOfficeId", + "transportationOfficeAssignments", "active", "roles", "edipi", @@ -6316,6 +6374,12 @@ func init() { "format": "telephone", "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$" }, + "transportationOfficeAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/TransportationOfficeAssignment" + } + }, "transportationOfficeId": { "type": "string", "format": "uuid" @@ -6372,10 +6436,11 @@ func init() { "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", "example": "212-555-5555" }, - "transportationOfficeId": { - "type": "string", - "format": "uuid", - "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + "transportationOfficeAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/OfficeUserTransportationOfficeAssignment" + } } } }, @@ -6413,6 +6478,22 @@ func init() { } } }, + "OfficeUserTransportationOfficeAssignment": { + "type": "object", + "properties": { + "primaryOffice": { + "type": "boolean", + "title": "primaryOffice", + "x-nullable": true + }, + "transportationOfficeId": { + "type": "string", + "format": "uuid", + "title": "transportationOfficeId", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + } + } + }, "OfficeUserUpdate": { "type": "object", "properties": { @@ -6455,10 +6536,11 @@ func init() { "x-nullable": true, "example": "212-555-5555" }, - "transportationOfficeId": { - "type": "string", - "format": "uuid", - "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + "transportationOfficeAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/OfficeUserTransportationOfficeAssignment" + } } } }, @@ -6784,6 +6866,38 @@ func init() { } } }, + "TransportationOfficeAssignment": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "officeUserId": { + "type": "string", + "format": "uuid", + "example": "c56a4780-65aa-42ec-a945-5fd87dec0538" + }, + "primaryOffice": { + "type": "boolean", + "x-omitempty": false + }, + "transportationOffice": { + "$ref": "#/definitions/TransportationOffice" + }, + "transportationOfficeId": { + "type": "string", + "format": "uuid", + "example": "d67a4780-65aa-42ec-a945-5fd87dec0549" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + } + }, "TransportationOffices": { "type": "array", "items": { diff --git a/pkg/gen/adminmessages/office_user.go b/pkg/gen/adminmessages/office_user.go index 648b2f3ddaa..0a58b7a0861 100644 --- a/pkg/gen/adminmessages/office_user.go +++ b/pkg/gen/adminmessages/office_user.go @@ -83,6 +83,10 @@ type OfficeUser struct { // Pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ Telephone *string `json:"telephone"` + // transportation office assignments + // Required: true + TransportationOfficeAssignments []*TransportationOfficeAssignment `json:"transportationOfficeAssignments"` + // transportation office Id // Required: true // Format: uuid @@ -159,6 +163,10 @@ func (m *OfficeUser) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateTransportationOfficeAssignments(formats); err != nil { + res = append(res, err) + } + if err := m.validateTransportationOfficeID(formats); err != nil { res = append(res, err) } @@ -391,6 +399,33 @@ func (m *OfficeUser) validateTelephone(formats strfmt.Registry) error { return nil } +func (m *OfficeUser) validateTransportationOfficeAssignments(formats strfmt.Registry) error { + + if err := validate.Required("transportationOfficeAssignments", "body", m.TransportationOfficeAssignments); err != nil { + return err + } + + for i := 0; i < len(m.TransportationOfficeAssignments); i++ { + if swag.IsZero(m.TransportationOfficeAssignments[i]) { // not required + continue + } + + if m.TransportationOfficeAssignments[i] != nil { + if err := m.TransportationOfficeAssignments[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + func (m *OfficeUser) validateTransportationOfficeID(formats strfmt.Registry) error { if err := validate.Required("transportationOfficeId", "body", m.TransportationOfficeID); err != nil { @@ -445,6 +480,10 @@ func (m *OfficeUser) ContextValidate(ctx context.Context, formats strfmt.Registr res = append(res, err) } + if err := m.contextValidateTransportationOfficeAssignments(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateUpdatedAt(ctx, formats); err != nil { res = append(res, err) } @@ -514,6 +553,31 @@ func (m *OfficeUser) contextValidateRoles(ctx context.Context, formats strfmt.Re return nil } +func (m *OfficeUser) contextValidateTransportationOfficeAssignments(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.TransportationOfficeAssignments); i++ { + + if m.TransportationOfficeAssignments[i] != nil { + + if swag.IsZero(m.TransportationOfficeAssignments[i]) { // not required + return nil + } + + if err := m.TransportationOfficeAssignments[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + func (m *OfficeUser) contextValidateUpdatedAt(ctx context.Context, formats strfmt.Registry) error { if err := validate.ReadOnly(ctx, "updatedAt", "body", strfmt.DateTime(m.UpdatedAt)); err != nil { diff --git a/pkg/gen/adminmessages/office_user_create.go b/pkg/gen/adminmessages/office_user_create.go index 404719daa25..6ad14306927 100644 --- a/pkg/gen/adminmessages/office_user_create.go +++ b/pkg/gen/adminmessages/office_user_create.go @@ -45,10 +45,8 @@ type OfficeUserCreate struct { // Pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ Telephone string `json:"telephone,omitempty"` - // transportation office Id - // Example: c56a4180-65aa-42ec-a945-5fd21dec0538 - // Format: uuid - TransportationOfficeID strfmt.UUID `json:"transportationOfficeId,omitempty"` + // transportation office assignments + TransportationOfficeAssignments []*OfficeUserTransportationOfficeAssignment `json:"transportationOfficeAssignments"` } // Validate validates this office user create @@ -67,7 +65,7 @@ func (m *OfficeUserCreate) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateTransportationOfficeID(formats); err != nil { + if err := m.validateTransportationOfficeAssignments(formats); err != nil { res = append(res, err) } @@ -141,13 +139,27 @@ func (m *OfficeUserCreate) validateTelephone(formats strfmt.Registry) error { return nil } -func (m *OfficeUserCreate) validateTransportationOfficeID(formats strfmt.Registry) error { - if swag.IsZero(m.TransportationOfficeID) { // not required +func (m *OfficeUserCreate) validateTransportationOfficeAssignments(formats strfmt.Registry) error { + if swag.IsZero(m.TransportationOfficeAssignments) { // not required return nil } - if err := validate.FormatOf("transportationOfficeId", "body", "uuid", m.TransportationOfficeID.String(), formats); err != nil { - return err + for i := 0; i < len(m.TransportationOfficeAssignments); i++ { + if swag.IsZero(m.TransportationOfficeAssignments[i]) { // not required + continue + } + + if m.TransportationOfficeAssignments[i] != nil { + if err := m.TransportationOfficeAssignments[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } + return err + } + } + } return nil @@ -165,6 +177,10 @@ func (m *OfficeUserCreate) ContextValidate(ctx context.Context, formats strfmt.R res = append(res, err) } + if err := m.contextValidateTransportationOfficeAssignments(ctx, formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -221,6 +237,31 @@ func (m *OfficeUserCreate) contextValidateRoles(ctx context.Context, formats str return nil } +func (m *OfficeUserCreate) contextValidateTransportationOfficeAssignments(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.TransportationOfficeAssignments); i++ { + + if m.TransportationOfficeAssignments[i] != nil { + + if swag.IsZero(m.TransportationOfficeAssignments[i]) { // not required + return nil + } + + if err := m.TransportationOfficeAssignments[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + // MarshalBinary interface implementation func (m *OfficeUserCreate) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/pkg/gen/adminmessages/office_user_transportation_office_assignment.go b/pkg/gen/adminmessages/office_user_transportation_office_assignment.go new file mode 100644 index 00000000000..ad1fcaffffb --- /dev/null +++ b/pkg/gen/adminmessages/office_user_transportation_office_assignment.go @@ -0,0 +1,78 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package adminmessages + +// 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" +) + +// OfficeUserTransportationOfficeAssignment office user transportation office assignment +// +// swagger:model OfficeUserTransportationOfficeAssignment +type OfficeUserTransportationOfficeAssignment struct { + + // primaryOffice + PrimaryOffice *bool `json:"primaryOffice,omitempty"` + + // transportationOfficeId + // Example: c56a4180-65aa-42ec-a945-5fd21dec0538 + // Format: uuid + TransportationOfficeID strfmt.UUID `json:"transportationOfficeId,omitempty"` +} + +// Validate validates this office user transportation office assignment +func (m *OfficeUserTransportationOfficeAssignment) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateTransportationOfficeID(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *OfficeUserTransportationOfficeAssignment) validateTransportationOfficeID(formats strfmt.Registry) error { + if swag.IsZero(m.TransportationOfficeID) { // not required + return nil + } + + if err := validate.FormatOf("transportationOfficeId", "body", "uuid", m.TransportationOfficeID.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this office user transportation office assignment based on context it is used +func (m *OfficeUserTransportationOfficeAssignment) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *OfficeUserTransportationOfficeAssignment) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *OfficeUserTransportationOfficeAssignment) UnmarshalBinary(b []byte) error { + var res OfficeUserTransportationOfficeAssignment + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/adminmessages/office_user_update.go b/pkg/gen/adminmessages/office_user_update.go index db9fbcb98ef..1210927a8e6 100644 --- a/pkg/gen/adminmessages/office_user_update.go +++ b/pkg/gen/adminmessages/office_user_update.go @@ -44,10 +44,8 @@ type OfficeUserUpdate struct { // Pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ Telephone *string `json:"telephone,omitempty"` - // transportation office Id - // Example: c56a4180-65aa-42ec-a945-5fd21dec0538 - // Format: uuid - TransportationOfficeID strfmt.UUID `json:"transportationOfficeId,omitempty"` + // transportation office assignments + TransportationOfficeAssignments []*OfficeUserTransportationOfficeAssignment `json:"transportationOfficeAssignments"` } // Validate validates this office user update @@ -66,7 +64,7 @@ func (m *OfficeUserUpdate) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateTransportationOfficeID(formats); err != nil { + if err := m.validateTransportationOfficeAssignments(formats); err != nil { res = append(res, err) } @@ -140,13 +138,27 @@ func (m *OfficeUserUpdate) validateTelephone(formats strfmt.Registry) error { return nil } -func (m *OfficeUserUpdate) validateTransportationOfficeID(formats strfmt.Registry) error { - if swag.IsZero(m.TransportationOfficeID) { // not required +func (m *OfficeUserUpdate) validateTransportationOfficeAssignments(formats strfmt.Registry) error { + if swag.IsZero(m.TransportationOfficeAssignments) { // not required return nil } - if err := validate.FormatOf("transportationOfficeId", "body", "uuid", m.TransportationOfficeID.String(), formats); err != nil { - return err + for i := 0; i < len(m.TransportationOfficeAssignments); i++ { + if swag.IsZero(m.TransportationOfficeAssignments[i]) { // not required + continue + } + + if m.TransportationOfficeAssignments[i] != nil { + if err := m.TransportationOfficeAssignments[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } + return err + } + } + } return nil @@ -164,6 +176,10 @@ func (m *OfficeUserUpdate) ContextValidate(ctx context.Context, formats strfmt.R res = append(res, err) } + if err := m.contextValidateTransportationOfficeAssignments(ctx, formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -220,6 +236,31 @@ func (m *OfficeUserUpdate) contextValidateRoles(ctx context.Context, formats str return nil } +func (m *OfficeUserUpdate) contextValidateTransportationOfficeAssignments(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.TransportationOfficeAssignments); i++ { + + if m.TransportationOfficeAssignments[i] != nil { + + if swag.IsZero(m.TransportationOfficeAssignments[i]) { // not required + return nil + } + + if err := m.TransportationOfficeAssignments[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOfficeAssignments" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + // MarshalBinary interface implementation func (m *OfficeUserUpdate) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/pkg/gen/adminmessages/transportation_office_assignment.go b/pkg/gen/adminmessages/transportation_office_assignment.go new file mode 100644 index 00000000000..8deb239d7a5 --- /dev/null +++ b/pkg/gen/adminmessages/transportation_office_assignment.go @@ -0,0 +1,223 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package adminmessages + +// 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" +) + +// TransportationOfficeAssignment transportation office assignment +// +// swagger:model TransportationOfficeAssignment +type TransportationOfficeAssignment struct { + + // created at + // Read Only: true + // Format: date-time + CreatedAt strfmt.DateTime `json:"createdAt,omitempty"` + + // office user Id + // Example: c56a4780-65aa-42ec-a945-5fd87dec0538 + // Format: uuid + OfficeUserID strfmt.UUID `json:"officeUserId,omitempty"` + + // primary office + PrimaryOffice bool `json:"primaryOffice"` + + // transportation office + TransportationOffice *TransportationOffice `json:"transportationOffice,omitempty"` + + // transportation office Id + // Example: d67a4780-65aa-42ec-a945-5fd87dec0549 + // Format: uuid + TransportationOfficeID strfmt.UUID `json:"transportationOfficeId,omitempty"` + + // updated at + // Read Only: true + // Format: date-time + UpdatedAt strfmt.DateTime `json:"updatedAt,omitempty"` +} + +// Validate validates this transportation office assignment +func (m *TransportationOfficeAssignment) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCreatedAt(formats); err != nil { + res = append(res, err) + } + + if err := m.validateOfficeUserID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTransportationOffice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTransportationOfficeID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateUpdatedAt(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *TransportationOfficeAssignment) validateCreatedAt(formats strfmt.Registry) error { + if swag.IsZero(m.CreatedAt) { // not required + return nil + } + + if err := validate.FormatOf("createdAt", "body", "date-time", m.CreatedAt.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *TransportationOfficeAssignment) validateOfficeUserID(formats strfmt.Registry) error { + if swag.IsZero(m.OfficeUserID) { // not required + return nil + } + + if err := validate.FormatOf("officeUserId", "body", "uuid", m.OfficeUserID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *TransportationOfficeAssignment) validateTransportationOffice(formats strfmt.Registry) error { + if swag.IsZero(m.TransportationOffice) { // not required + return nil + } + + if m.TransportationOffice != nil { + if err := m.TransportationOffice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOffice") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOffice") + } + return err + } + } + + return nil +} + +func (m *TransportationOfficeAssignment) validateTransportationOfficeID(formats strfmt.Registry) error { + if swag.IsZero(m.TransportationOfficeID) { // not required + return nil + } + + if err := validate.FormatOf("transportationOfficeId", "body", "uuid", m.TransportationOfficeID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *TransportationOfficeAssignment) validateUpdatedAt(formats strfmt.Registry) error { + if swag.IsZero(m.UpdatedAt) { // not required + return nil + } + + if err := validate.FormatOf("updatedAt", "body", "date-time", m.UpdatedAt.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this transportation office assignment based on the context it is used +func (m *TransportationOfficeAssignment) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateCreatedAt(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateTransportationOffice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateUpdatedAt(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *TransportationOfficeAssignment) contextValidateCreatedAt(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "createdAt", "body", strfmt.DateTime(m.CreatedAt)); err != nil { + return err + } + + return nil +} + +func (m *TransportationOfficeAssignment) contextValidateTransportationOffice(ctx context.Context, formats strfmt.Registry) error { + + if m.TransportationOffice != nil { + + if swag.IsZero(m.TransportationOffice) { // not required + return nil + } + + if err := m.TransportationOffice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("transportationOffice") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("transportationOffice") + } + return err + } + } + + return nil +} + +func (m *TransportationOfficeAssignment) contextValidateUpdatedAt(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "updatedAt", "body", strfmt.DateTime(m.UpdatedAt)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *TransportationOfficeAssignment) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *TransportationOfficeAssignment) UnmarshalBinary(b []byte) error { + var res TransportationOfficeAssignment + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/handlers/adminapi/api.go b/pkg/handlers/adminapi/api.go index 231ded61fe7..7e641a66c2d 100644 --- a/pkg/handlers/adminapi/api.go +++ b/pkg/handlers/adminapi/api.go @@ -25,6 +25,7 @@ import ( requestedofficeusers "github.com/transcom/mymove/pkg/services/requested_office_users" "github.com/transcom/mymove/pkg/services/roles" signedcertification "github.com/transcom/mymove/pkg/services/signed_certification" + transportationofficeassignments "github.com/transcom/mymove/pkg/services/transportation_office_assignments" "github.com/transcom/mymove/pkg/services/upload" user "github.com/transcom/mymove/pkg/services/user" usersprivileges "github.com/transcom/mymove/pkg/services/users_privileges" @@ -81,11 +82,12 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { adminAPI.OfficeUsersGetOfficeUserHandler = GetOfficeUserHandler{ handlerConfig, - officeuser.NewOfficeUserFetcher(queryBuilder), + officeuser.NewOfficeUserFetcherPop(), query.NewQueryFilter, } userPrivilegesCreator := usersprivileges.NewUsersPrivilegesCreator() + transportaionOfficeAssignmentUpdater := transportationofficeassignments.NewTransportaionOfficeAssignmentUpdater() adminAPI.OfficeUsersCreateOfficeUserHandler = CreateOfficeUserHandler{ handlerConfig, officeuser.NewOfficeUserCreator(queryBuilder, handlerConfig.NotificationSender()), @@ -93,6 +95,7 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { userRolesCreator, newRolesFetcher, userPrivilegesCreator, + transportaionOfficeAssignmentUpdater, } adminAPI.OfficeUsersUpdateOfficeUserHandler = UpdateOfficeUserHandler{ @@ -102,6 +105,7 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { userRolesCreator, userPrivilegesCreator, user.NewUserSessionRevocation(queryBuilder), + transportaionOfficeAssignmentUpdater, } adminAPI.TransportationOfficesIndexOfficesHandler = IndexOfficesHandler{ diff --git a/pkg/handlers/adminapi/office_users.go b/pkg/handlers/adminapi/office_users.go index 27c2e4ea382..790ab1a26b0 100644 --- a/pkg/handlers/adminapi/office_users.go +++ b/pkg/handlers/adminapi/office_users.go @@ -32,13 +32,23 @@ func payloadForRole(r roles.Role) *adminmessages.Role { } } -func payloadForPrivilege(r models.Privilege) *adminmessages.Privilege { +func payloadForPrivilege(p models.Privilege) *adminmessages.Privilege { return &adminmessages.Privilege{ - ID: *handlers.FmtUUID(r.ID), - PrivilegeType: *handlers.FmtString(string(r.PrivilegeType)), - PrivilegeName: *handlers.FmtString(string(r.PrivilegeName)), - CreatedAt: *handlers.FmtDateTime(r.CreatedAt), - UpdatedAt: *handlers.FmtDateTime(r.UpdatedAt), + ID: *handlers.FmtUUID(p.ID), + PrivilegeType: *handlers.FmtString(string(p.PrivilegeType)), + PrivilegeName: *handlers.FmtString(string(p.PrivilegeName)), + CreatedAt: *handlers.FmtDateTime(p.CreatedAt), + UpdatedAt: *handlers.FmtDateTime(p.UpdatedAt), + } +} + +func payloadForTransportationOfficeAssignment(toa models.TransportationOfficeAssignment) *adminmessages.TransportationOfficeAssignment { + return &adminmessages.TransportationOfficeAssignment{ + OfficeUserID: *handlers.FmtUUID(toa.ID), + TransportationOfficeID: *handlers.FmtUUID(toa.TransportationOfficeID), + PrimaryOffice: *handlers.FmtBool(toa.PrimaryOffice), + CreatedAt: *handlers.FmtDateTime(toa.CreatedAt), + UpdatedAt: *handlers.FmtDateTime(toa.UpdatedAt), } } @@ -72,6 +82,9 @@ func payloadForOfficeUserModel(o models.OfficeUser) *adminmessages.OfficeUser { for _, privilege := range user.Privileges { payload.Privileges = append(payload.Privileges, payloadForPrivilege(privilege)) } + for _, transportationAssignment := range o.TransportationOfficeAssignments { + payload.TransportationOfficeAssignments = append(payload.TransportationOfficeAssignments, payloadForTransportationOfficeAssignment(transportationAssignment)) + } return payload } @@ -138,7 +151,7 @@ func (h IndexOfficeUsersHandler) Handle(params officeuserop.IndexOfficeUsersPara // GetOfficeUserHandler retrieves office user handler type GetOfficeUserHandler struct { handlers.HandlerConfig - services.OfficeUserFetcher + services.OfficeUserFetcherPop services.NewQueryFilter } @@ -147,18 +160,17 @@ func (h GetOfficeUserHandler) Handle(params officeuserop.GetOfficeUserParams) mi return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - officeUserID := params.OfficeUserID - - queryFilters := []services.QueryFilter{query.NewQueryFilter("id", "=", officeUserID)} - - officeUser, err := h.OfficeUserFetcher.FetchOfficeUser(appCtx, queryFilters) + officeUserID := uuid.FromStringOrNil(params.OfficeUserID.String()) + officeUser, err := h.OfficeUserFetcherPop.FetchOfficeUserByID(appCtx, officeUserID) if err != nil { return handlers.ResponseForError(appCtx.Logger(), err), err } + userError := appCtx.DB().Load(&officeUser, "User") if userError != nil { return handlers.ResponseForError(appCtx.Logger(), userError), userError } + //todo: we want to move this query out of the handler and into querybuilder, if possible roleError := appCtx.DB().Q().Join("users_roles", "users_roles.role_id = roles.id"). Where("users_roles.deleted_at IS NULL AND users_roles.user_id = ?", (officeUser.User.ID)). @@ -166,12 +178,22 @@ func (h GetOfficeUserHandler) Handle(params officeuserop.GetOfficeUserParams) mi if roleError != nil { return handlers.ResponseForError(appCtx.Logger(), roleError), roleError } + privilegeError := appCtx.DB().Q().Join("users_privileges", "users_privileges.privilege_id = privileges.id"). Where("users_privileges.deleted_at IS NULL AND users_privileges.user_id = ?", (officeUser.User.ID)). All(&officeUser.User.Privileges) if privilegeError != nil { return handlers.ResponseForError(appCtx.Logger(), privilegeError), privilegeError } + + transportationOfficeAssignmentError := appCtx.DB().Q().EagerPreload("TransportationOffice"). + Join("transportation_offices", "transportation_office_assignments.transportation_office_id = transportation_offices.id"). + Where("transportation_office_assignments.id = ?", (officeUser.ID)). + All(&officeUser.TransportationOfficeAssignments) + if transportationOfficeAssignmentError != nil { + return handlers.ResponseForError(appCtx.Logger(), transportationOfficeAssignmentError), transportationOfficeAssignmentError + } + payload := payloadForOfficeUserModel(officeUser) return officeuserop.NewGetOfficeUserOK().WithPayload(payload), nil @@ -186,6 +208,7 @@ type CreateOfficeUserHandler struct { services.UserRoleAssociator services.RoleAssociater services.UserPrivilegeAssociator + services.TransportaionOfficeAssignmentUpdater } // Handle creates an office user @@ -194,9 +217,17 @@ func (h CreateOfficeUserHandler) Handle(params officeuserop.CreateOfficeUserPara return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - transportationOfficeID, err := uuid.FromString(payload.TransportationOfficeID.String()) + if len(payload.TransportationOfficeAssignments) == 0 { + err := apperror.NewBadDataError("At least one transportation office is required") + appCtx.Logger().Error(err.Error()) + return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err + } + + primaryTransportationOfficeID, err := getPrimaryTransportationOfficeIDFromPayload(payload.TransportationOfficeAssignments) + if err != nil { - appCtx.Logger().Error(fmt.Sprintf("UUID Parsing for %s", payload.TransportationOfficeID.String()), zap.Error(err)) + appCtx.Logger().Error("Error identifying primary transportation office", zap.Error(err)) + appCtx.Logger().Error(err.Error()) return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err } @@ -221,16 +252,16 @@ func (h CreateOfficeUserHandler) Handle(params officeuserop.CreateOfficeUserPara FirstName: payload.FirstName, Telephone: payload.Telephone, Email: payload.Email, - TransportationOfficeID: transportationOfficeID, + TransportationOfficeID: primaryTransportationOfficeID, Active: true, Status: &officeUserStatus, } - transportationIDFilter := []services.QueryFilter{ - h.NewQueryFilter("id", "=", transportationOfficeID), + primaryTransportationIDFilter := []services.QueryFilter{ + h.NewQueryFilter("id", "=", primaryTransportationOfficeID), } - createdOfficeUser, verrs, err := h.OfficeUserCreator.CreateOfficeUser(appCtx, &officeUser, transportationIDFilter) + createdOfficeUser, verrs, err := h.OfficeUserCreator.CreateOfficeUser(appCtx, &officeUser, primaryTransportationIDFilter) if verrs != nil { validationError := &adminmessages.ValidationError{ InvalidFields: handlers.NewValidationErrorsResponse(verrs).Errors, @@ -280,6 +311,21 @@ func (h CreateOfficeUserHandler) Handle(params officeuserop.CreateOfficeUserPara return officeuserop.NewUpdateOfficeUserInternalServerError(), err } + updatedTransportationOfficeAssignments, err := transportationOfficeAssignmentsPayloadToModel(payload.TransportationOfficeAssignments) + if err != nil { + appCtx.Logger().Error("UUID parsing error for transportation office assignments", zap.Error(err)) + return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err + } + + transportationOfficeAssignments, err := + h.TransportaionOfficeAssignmentUpdater.UpdateTransportaionOfficeAssignments(appCtx, createdOfficeUser.ID, updatedTransportationOfficeAssignments) + if err != nil { + appCtx.Logger().Error("Error updating office user's transportation office assignments", zap.Error(err)) + return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err + } + + createdOfficeUser.TransportationOfficeAssignments = transportationOfficeAssignments + _, err = audit.Capture(appCtx, createdOfficeUser, nil, params.HTTPRequest) if err != nil { appCtx.Logger().Error("Error capturing audit record", zap.Error(err)) @@ -298,6 +344,7 @@ type UpdateOfficeUserHandler struct { services.UserRoleAssociator services.UserPrivilegeAssociator services.UserSessionRevocation + services.TransportaionOfficeAssignmentUpdater } // Handle updates an office user @@ -310,12 +357,23 @@ func (h UpdateOfficeUserHandler) Handle(params officeuserop.UpdateOfficeUserPara appCtx.Logger().Error(fmt.Sprintf("UUID Parsing for %s", params.OfficeUserID.String()), zap.Error(err)) } - updatedOfficeUser, verrs, err := h.OfficeUserUpdater.UpdateOfficeUser(appCtx, officeUserID, payload) + var primaryTransportationOfficeID uuid.UUID + if len(payload.TransportationOfficeAssignments) > 0 { + primaryTransportationOfficeID, err = getPrimaryTransportationOfficeIDFromPayload(payload.TransportationOfficeAssignments) + + if err != nil { + appCtx.Logger().Error("Error identifying primary transportation office", zap.Error(err)) + return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err + } + } + + updatedOfficeUser, verrs, err := h.OfficeUserUpdater.UpdateOfficeUser(appCtx, officeUserID, payload, primaryTransportationOfficeID) if err != nil || verrs != nil { appCtx.Logger().Error("Error saving user", zap.Error(err), zap.Error(verrs)) return officeuserop.NewUpdateOfficeUserInternalServerError(), err } + if updatedOfficeUser.UserID != nil && payload.Roles != nil { updatedRoles := rolesPayloadToModel(payload.Roles) _, verrs, err = h.UserRoleAssociator.UpdateUserRoles(appCtx, *updatedOfficeUser.UserID, updatedRoles) @@ -393,6 +451,48 @@ func (h UpdateOfficeUserHandler) Handle(params officeuserop.UpdateOfficeUserPara } } + if len(payload.TransportationOfficeAssignments) > 0 { + + transportationOfficeAssignmentsFromPayload, err := transportationOfficeAssignmentsPayloadToModel(payload.TransportationOfficeAssignments) + if err != nil { + appCtx.Logger().Error("UUID parsing error for transportation office assignments", zap.Error(err)) + return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err + } + + updatedTransportationOfficeAssignments, err := + h.TransportaionOfficeAssignmentUpdater.UpdateTransportaionOfficeAssignments(appCtx, updatedOfficeUser.ID, transportationOfficeAssignmentsFromPayload) + if err != nil { + appCtx.Logger().Error("Error updating office user's transportation office assignments", zap.Error(err)) + return officeuserop.NewCreateOfficeUserUnprocessableEntity(), err + } + + updatedOfficeUser.TransportationOfficeAssignments = updatedTransportationOfficeAssignments + + boolean := true + revokeOfficeSessionPayload := adminmessages.UserUpdate{ + RevokeOfficeSession: &boolean, + } + + _, validationErrors, revokeErr := h.UserSessionRevocation.RevokeUserSession( + appCtx, + *updatedOfficeUser.UserID, + &revokeOfficeSessionPayload, + h.SessionManagers(), + ) + + if revokeErr != nil { + err = apperror.NewInternalServerError("Error revoking user session") + appCtx.Logger().Error(err.Error(), zap.Error(revokeErr)) + return userop.NewUpdateUserInternalServerError(), revokeErr + } + + if validationErrors != nil { + err = apperror.NewInternalServerError("Error revoking user session") + appCtx.Logger().Error(err.Error(), zap.Error(verrs)) + return userop.NewUpdateUserInternalServerError(), validationErrors + } + } + // Log if the account was enabled or disabled (POAM requirement) if payload.Active != nil { _, err = audit.CaptureAccountStatus(appCtx, updatedOfficeUser, *payload.Active, params.HTTPRequest) @@ -431,3 +531,41 @@ func privilegesPayloadToModel(payload []*adminmessages.OfficeUserPrivilege) []mo } return rt } + +func transportationOfficeAssignmentsPayloadToModel(payload []*adminmessages.OfficeUserTransportationOfficeAssignment) (models.TransportationOfficeAssignments, error) { + var toas models.TransportationOfficeAssignments + for _, toa := range payload { + transportationOfficeID, err := uuid.FromString(toa.TransportationOfficeID.String()) + + if err != nil { + return models.TransportationOfficeAssignments{}, err + } + + model := &models.TransportationOfficeAssignment{ + TransportationOfficeID: transportationOfficeID, + PrimaryOffice: *toa.PrimaryOffice, + } + + toas = append(toas, *model) + } + return toas, nil +} + +func getPrimaryTransportationOfficeIDFromPayload(payload []*adminmessages.OfficeUserTransportationOfficeAssignment) (uuid.UUID, error) { + var transportationOfficeID uuid.UUID + var err error + + if len(payload) == 1 { + transportationOfficeID, err = uuid.FromString(payload[0].TransportationOfficeID.String()) + return transportationOfficeID, err + } + + for _, toa := range payload { + if toa.PrimaryOffice != nil && *toa.PrimaryOffice { + transportationOfficeID, err = uuid.FromString(toa.TransportationOfficeID.String()) + return transportationOfficeID, err + } + } + + return transportationOfficeID, apperror.NewBadDataError("Could not identify primary transportaion office from list of assignments") +} diff --git a/pkg/handlers/adminapi/office_users_test.go b/pkg/handlers/adminapi/office_users_test.go index e5af2e50b38..e6660893810 100644 --- a/pkg/handlers/adminapi/office_users_test.go +++ b/pkg/handlers/adminapi/office_users_test.go @@ -22,6 +22,7 @@ import ( "github.com/transcom/mymove/pkg/services/pagination" "github.com/transcom/mymove/pkg/services/query" rolesservice "github.com/transcom/mymove/pkg/services/roles" + transportaionofficeassignments "github.com/transcom/mymove/pkg/services/transportation_office_assignments" usersprivileges "github.com/transcom/mymove/pkg/services/users_privileges" usersroles "github.com/transcom/mymove/pkg/services/users_roles" ) @@ -97,10 +98,10 @@ func (suite *HandlerSuite) TestGetOfficeUserHandler() { OfficeUserID: strfmt.UUID(officeUser.ID.String()), } - queryBuilder := query.NewQueryBuilder() + // queryBuilder := query.NewQueryBuilder() handler := GetOfficeUserHandler{ suite.HandlerConfig(), - officeuser.NewOfficeUserFetcher(queryBuilder), + officeuser.NewOfficeUserFetcherPop(), query.NewQueryFilter, } @@ -121,10 +122,10 @@ func (suite *HandlerSuite) TestGetOfficeUserHandler() { OfficeUserID: strfmt.UUID(fakeID), } - queryBuilder := query.NewQueryBuilder() + // queryBuilder := query.NewQueryBuilder() handler := GetOfficeUserHandler{ suite.HandlerConfig(), - officeuser.NewOfficeUserFetcher(queryBuilder), + officeuser.NewOfficeUserFetcherPop(), query.NewQueryFilter, } @@ -151,6 +152,7 @@ func (suite *HandlerSuite) TestCreateOfficeUserHandler() { // Set up: Create a new Office User, save new user to the DB // Expected Outcome: The office user is created and we get a 200 OK. transportationOfficeID := factory.BuildDefaultTransportationOffice(suite.DB()).ID + primaryOffice := true params := officeuserop.CreateOfficeUserParams{ HTTPRequest: suite.setupAuthenticatedRequest("POST", "/office_users"), @@ -175,7 +177,12 @@ func (suite *HandlerSuite) TestCreateOfficeUserHandler() { PrivilegeType: &supervisorPrivilegeType, }, }, - TransportationOfficeID: strfmt.UUID(transportationOfficeID.String()), + TransportationOfficeAssignments: []*adminmessages.OfficeUserTransportationOfficeAssignment{ + { + TransportationOfficeID: strfmt.UUID(transportationOfficeID.String()), + PrimaryOffice: &primaryOffice, + }, + }, }, } queryBuilder := query.NewQueryBuilder() @@ -186,6 +193,7 @@ func (suite *HandlerSuite) TestCreateOfficeUserHandler() { usersroles.NewUsersRolesCreator(), rolesservice.NewRolesFetcher(), usersprivileges.NewUsersPrivilegesCreator(), + transportaionofficeassignments.NewTransportaionOfficeAssignmentUpdater(), } suite.NoError(params.OfficeUser.Validate(strfmt.Default)) response := handler.Handle(params) @@ -197,6 +205,7 @@ func (suite *HandlerSuite) TestCreateOfficeUserHandler() { // Set up: Add new Office User to the DB // Expected Outcome: The office user is not created and we get a 500 internal server error. fakeTransportationOfficeID := "3b9c2975-4e54-40ea-a781-bab7d6e4a502" + primaryOffice := true officeUser := factory.BuildOfficeUser(suite.DB(), nil, []factory.Trait{ factory.GetTraitOfficeUserWithID, }) @@ -223,7 +232,12 @@ func (suite *HandlerSuite) TestCreateOfficeUserHandler() { PrivilegeType: &supervisorPrivilegeType, }, }, - TransportationOfficeID: strfmt.UUID(fakeTransportationOfficeID), + TransportationOfficeAssignments: []*adminmessages.OfficeUserTransportationOfficeAssignment{ + { + TransportationOfficeID: strfmt.UUID(fakeTransportationOfficeID), + PrimaryOffice: &primaryOffice, + }, + }, }, } @@ -235,6 +249,7 @@ func (suite *HandlerSuite) TestCreateOfficeUserHandler() { usersroles.NewUsersRolesCreator(), rolesservice.NewRolesFetcher(), usersprivileges.NewUsersPrivilegesCreator(), + transportaionofficeassignments.NewTransportaionOfficeAssignmentUpdater(), } response := handler.Handle(params) @@ -252,6 +267,7 @@ func (suite *HandlerSuite) TestUpdateOfficeUserHandler() { usersroles.NewUsersRolesCreator(), // a special can of worms, TODO mocked tests usersprivileges.NewUsersPrivilegesCreator(), revoker, + transportaionofficeassignments.NewTransportaionOfficeAssignmentUpdater(), } } @@ -268,15 +284,21 @@ func (suite *HandlerSuite) TestUpdateOfficeUserHandler() { suite.Run("Office user is successfully updated", func() { officeUser := setupTestData() transportationOffice := factory.BuildTransportationOffice(nil, nil, nil) + primaryOffice := true firstName := "Riley" middleInitials := "RB" telephone := "865-555-5309" officeUserUpdates := &adminmessages.OfficeUserUpdate{ - FirstName: &firstName, - MiddleInitials: &middleInitials, - Telephone: &telephone, - TransportationOfficeID: strfmt.UUID(transportationOffice.ID.String()), + FirstName: &firstName, + MiddleInitials: &middleInitials, + Telephone: &telephone, + TransportationOfficeAssignments: []*adminmessages.OfficeUserTransportationOfficeAssignment{ + { + TransportationOfficeID: strfmt.UUID(transportationOffice.ID.String()), + PrimaryOffice: &primaryOffice, + }, + }, } params := officeuserop.UpdateOfficeUserParams{ @@ -312,8 +334,14 @@ func (suite *HandlerSuite) TestUpdateOfficeUserHandler() { suite.Run("Update fails due to bad Transportation Office", func() { officeUser := setupTestData() + primaryOffice := true officeUserUpdates := &adminmessages.OfficeUserUpdate{ - TransportationOfficeID: strfmt.UUID(uuid.Must(uuid.NewV4()).String()), + TransportationOfficeAssignments: []*adminmessages.OfficeUserTransportationOfficeAssignment{ + { + TransportationOfficeID: strfmt.UUID(uuid.Must(uuid.NewV4()).String()), + PrimaryOffice: &primaryOffice, + }, + }, } params := officeuserop.UpdateOfficeUserParams{ diff --git a/pkg/models/office_user.go b/pkg/models/office_user.go index 1857e10ed84..a09258acf0d 100644 --- a/pkg/models/office_user.go +++ b/pkg/models/office_user.go @@ -24,23 +24,24 @@ const ( // OfficeUser is someone who works in one of the TransportationOffices type OfficeUser struct { - ID uuid.UUID `json:"id" db:"id"` - UserID *uuid.UUID `json:"user_id" db:"user_id"` - User User `belongs_to:"user" fk_id:"user_id"` - LastName string `json:"last_name" db:"last_name"` - FirstName string `json:"first_name" db:"first_name"` - MiddleInitials *string `json:"middle_initials" db:"middle_initials"` - Email string `json:"email" db:"email"` - Telephone string `json:"telephone" db:"telephone"` - TransportationOfficeID uuid.UUID `json:"transportation_office_id" db:"transportation_office_id"` - TransportationOffice TransportationOffice `belongs_to:"transportation_office" fk_id:"transportation_office_id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Active bool `json:"active" db:"active"` - Status *OfficeUserStatus `json:"status" db:"status"` - EDIPI *string `json:"edipi" db:"edipi"` - OtherUniqueID *string `json:"other_unique_id" db:"other_unique_id"` - RejectionReason *string `json:"rejection_reason" db:"rejection_reason"` + ID uuid.UUID `json:"id" db:"id"` + UserID *uuid.UUID `json:"user_id" db:"user_id"` + User User `belongs_to:"user" fk_id:"user_id"` + LastName string `json:"last_name" db:"last_name"` + FirstName string `json:"first_name" db:"first_name"` + MiddleInitials *string `json:"middle_initials" db:"middle_initials"` + Email string `json:"email" db:"email"` + Telephone string `json:"telephone" db:"telephone"` + TransportationOfficeID uuid.UUID `json:"transportation_office_id" db:"transportation_office_id"` + TransportationOffice TransportationOffice `belongs_to:"transportation_office" fk_id:"transportation_office_id"` + TransportationOfficeAssignments TransportationOfficeAssignments `many_to_many:"transportation_office_assignments" primary_id:"id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Active bool `json:"active" db:"active"` + Status *OfficeUserStatus `json:"status" db:"status"` + EDIPI *string `json:"edipi" db:"edipi"` + OtherUniqueID *string `json:"other_unique_id" db:"other_unique_id"` + RejectionReason *string `json:"rejection_reason" db:"rejection_reason"` } // TableName overrides the table name used by Pop. diff --git a/pkg/models/transportation_office_assignments.go b/pkg/models/transportation_office_assignments.go index 99960ff48b8..6f3c5df292b 100644 --- a/pkg/models/transportation_office_assignments.go +++ b/pkg/models/transportation_office_assignments.go @@ -10,7 +10,7 @@ import ( ) // TransportationAssignment is the transportation office the OfficeUser is assigned to -type TransportationAssignment struct { +type TransportationOfficeAssignment struct { ID uuid.UUID `json:"id" db:"id"` TransportationOfficeID uuid.UUID `json:"transportation_office_id" db:"transportation_office_id"` TransportationOffice TransportationOffice `belongs_to:"transportation_office" fk_id:"transportation_office_id"` @@ -20,15 +20,15 @@ type TransportationAssignment struct { } // TableName overrides the table name used by Pop. -func (o TransportationAssignment) TableName() string { +func (t TransportationOfficeAssignment) TableName() string { return "transportation_office_assignments" } -type TransportationAssignments []TransportationAssignment +type TransportationOfficeAssignments []TransportationOfficeAssignment // Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. -func (o *TransportationAssignment) Validate(_ *pop.Connection) (*validate.Errors, error) { +func (t *TransportationOfficeAssignment) Validate(_ *pop.Connection) (*validate.Errors, error) { return validate.Validate( - &validators.UUIDIsPresent{Field: o.TransportationOfficeID, Name: "TransportationOfficeID"}, + &validators.UUIDIsPresent{Field: t.TransportationOfficeID, Name: "TransportationOfficeID"}, ), nil } diff --git a/pkg/models/user.go b/pkg/models/user.go index e895fe01fcd..fec1509f958 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -103,26 +103,27 @@ func UpdateUserOktaID(db *pop.Connection, user *User, oktaID string) error { // UserIdentity is summary of the information about a user from the database type UserIdentity struct { - ID uuid.UUID `db:"id"` - Active bool `db:"active"` - Email string `db:"email"` - ServiceMemberID *uuid.UUID `db:"sm_id"` - ServiceMemberFirstName *string `db:"sm_fname"` - ServiceMemberLastName *string `db:"sm_lname"` - ServiceMemberMiddle *string `db:"sm_middle"` - ServiceMemberCacValidated *bool `db:"sm_cac_validated"` - OfficeUserID *uuid.UUID `db:"ou_id"` - OfficeUserFirstName *string `db:"ou_fname"` - OfficeUserLastName *string `db:"ou_lname"` - OfficeUserMiddle *string `db:"ou_middle"` - OfficeActive *bool `db:"ou_active"` - AdminUserID *uuid.UUID `db:"au_id"` - AdminUserRole *AdminRole `db:"au_role"` - AdminUserFirstName *string `db:"au_fname"` - AdminUserLastName *string `db:"au_lname"` - AdminUserActive *bool `db:"au_active"` - Roles roles.Roles `many_to_many:"users_roles" primary_id:"user_id"` - Privileges Privileges `many_to_many:"users_privileges" primary_id:"user_id"` + ID uuid.UUID `db:"id"` + Active bool `db:"active"` + Email string `db:"email"` + ServiceMemberID *uuid.UUID `db:"sm_id"` + ServiceMemberFirstName *string `db:"sm_fname"` + ServiceMemberLastName *string `db:"sm_lname"` + ServiceMemberMiddle *string `db:"sm_middle"` + ServiceMemberCacValidated *bool `db:"sm_cac_validated"` + OfficeUserID *uuid.UUID `db:"ou_id"` + OfficeUserFirstName *string `db:"ou_fname"` + OfficeUserLastName *string `db:"ou_lname"` + OfficeUserMiddle *string `db:"ou_middle"` + OfficeActive *bool `db:"ou_active"` + AdminUserID *uuid.UUID `db:"au_id"` + AdminUserRole *AdminRole `db:"au_role"` + AdminUserFirstName *string `db:"au_fname"` + AdminUserLastName *string `db:"au_lname"` + AdminUserActive *bool `db:"au_active"` + Roles roles.Roles `many_to_many:"users_roles" primary_id:"user_id"` + Privileges Privileges `many_to_many:"users_privileges" primary_id:"user_id"` + TransportationOfficeAssignments TransportationOfficeAssignments `many_to_many:"transportation_office_assignmentss" primary_id:"id"` } // FetchUserIdentity queries the database for information about the logged in user @@ -170,6 +171,12 @@ func FetchUserIdentity(db *pop.Connection, oktaID string) (*UserIdentity, error) if privilegeError != nil { return nil, privilegeError } + transportationOfficeAssignmentError := db.EagerPreload("TransportationOffice"). + Join("transportation_offices", "transportation_office_assignments.transportation_office_id = transportation_offices.id"). + Where("transportation_office_assignments.id = ?", identity.OfficeUserID).All(&identity.TransportationOfficeAssignments) + if transportationOfficeAssignmentError != nil { + return nil, transportationOfficeAssignmentError + } return identity, nil } diff --git a/pkg/services/mocks/OfficeUserUpdater.go b/pkg/services/mocks/OfficeUserUpdater.go index bc236493d27..5a884f836a2 100644 --- a/pkg/services/mocks/OfficeUserUpdater.go +++ b/pkg/services/mocks/OfficeUserUpdater.go @@ -20,9 +20,9 @@ type OfficeUserUpdater struct { mock.Mock } -// UpdateOfficeUser provides a mock function with given fields: appCtx, id, payload -func (_m *OfficeUserUpdater) UpdateOfficeUser(appCtx appcontext.AppContext, id uuid.UUID, payload *adminmessages.OfficeUserUpdate) (*models.OfficeUser, *validate.Errors, error) { - ret := _m.Called(appCtx, id, payload) +// UpdateOfficeUser provides a mock function with given fields: appCtx, id, payload, primaryTransportationOfficeId +func (_m *OfficeUserUpdater) UpdateOfficeUser(appCtx appcontext.AppContext, id uuid.UUID, payload *adminmessages.OfficeUserUpdate, primaryTransportationOfficeId uuid.UUID) (*models.OfficeUser, *validate.Errors, error) { + ret := _m.Called(appCtx, id, payload, primaryTransportationOfficeId) if len(ret) == 0 { panic("no return value specified for UpdateOfficeUser") @@ -31,27 +31,27 @@ func (_m *OfficeUserUpdater) UpdateOfficeUser(appCtx appcontext.AppContext, id u var r0 *models.OfficeUser var r1 *validate.Errors var r2 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate) (*models.OfficeUser, *validate.Errors, error)); ok { - return rf(appCtx, id, payload) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate, uuid.UUID) (*models.OfficeUser, *validate.Errors, error)); ok { + return rf(appCtx, id, payload, primaryTransportationOfficeId) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate) *models.OfficeUser); ok { - r0 = rf(appCtx, id, payload) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate, uuid.UUID) *models.OfficeUser); ok { + r0 = rf(appCtx, id, payload, primaryTransportationOfficeId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.OfficeUser) } } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate) *validate.Errors); ok { - r1 = rf(appCtx, id, payload) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate, uuid.UUID) *validate.Errors); ok { + r1 = rf(appCtx, id, payload, primaryTransportationOfficeId) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*validate.Errors) } } - if rf, ok := ret.Get(2).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate) error); ok { - r2 = rf(appCtx, id, payload) + if rf, ok := ret.Get(2).(func(appcontext.AppContext, uuid.UUID, *adminmessages.OfficeUserUpdate, uuid.UUID) error); ok { + r2 = rf(appCtx, id, payload, primaryTransportationOfficeId) } else { r2 = ret.Error(2) } diff --git a/pkg/services/mocks/TransportaionOfficeAssignmentUpdater.go b/pkg/services/mocks/TransportaionOfficeAssignmentUpdater.go new file mode 100644 index 00000000000..2a0995c7fe7 --- /dev/null +++ b/pkg/services/mocks/TransportaionOfficeAssignmentUpdater.go @@ -0,0 +1,61 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + uuid "github.com/gofrs/uuid" +) + +// TransportaionOfficeAssignmentUpdater is an autogenerated mock type for the TransportaionOfficeAssignmentUpdater type +type TransportaionOfficeAssignmentUpdater struct { + mock.Mock +} + +// UpdateTransportaionOfficeAssignments provides a mock function with given fields: appCtx, officeUserId, transportationOfficeAssignments +func (_m *TransportaionOfficeAssignmentUpdater) UpdateTransportaionOfficeAssignments(appCtx appcontext.AppContext, officeUserId uuid.UUID, transportationOfficeAssignments models.TransportationOfficeAssignments) (models.TransportationOfficeAssignments, error) { + ret := _m.Called(appCtx, officeUserId, transportationOfficeAssignments) + + if len(ret) == 0 { + panic("no return value specified for UpdateTransportaionOfficeAssignments") + } + + var r0 models.TransportationOfficeAssignments + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, models.TransportationOfficeAssignments) (models.TransportationOfficeAssignments, error)); ok { + return rf(appCtx, officeUserId, transportationOfficeAssignments) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, models.TransportationOfficeAssignments) models.TransportationOfficeAssignments); ok { + r0 = rf(appCtx, officeUserId, transportationOfficeAssignments) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.TransportationOfficeAssignments) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, uuid.UUID, models.TransportationOfficeAssignments) error); ok { + r1 = rf(appCtx, officeUserId, transportationOfficeAssignments) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewTransportaionOfficeAssignmentUpdater creates a new instance of TransportaionOfficeAssignmentUpdater. 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 NewTransportaionOfficeAssignmentUpdater(t interface { + mock.TestingT + Cleanup(func()) +}) *TransportaionOfficeAssignmentUpdater { + mock := &TransportaionOfficeAssignmentUpdater{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/office_user.go b/pkg/services/office_user.go index 9e7a17d6bbe..67a0f62d271 100644 --- a/pkg/services/office_user.go +++ b/pkg/services/office_user.go @@ -40,9 +40,9 @@ type OfficeUserCreator interface { CreateOfficeUser(appCtx appcontext.AppContext, user *models.OfficeUser, transportationIDFilter []QueryFilter) (*models.OfficeUser, *validate.Errors, error) } -// OfficeUserUpdater is the exported interface for creating an office user +// OfficeUserUpdater is the exported interface for updating an office user // //go:generate mockery --name OfficeUserUpdater type OfficeUserUpdater interface { - UpdateOfficeUser(appCtx appcontext.AppContext, id uuid.UUID, payload *adminmessages.OfficeUserUpdate) (*models.OfficeUser, *validate.Errors, error) + UpdateOfficeUser(appCtx appcontext.AppContext, id uuid.UUID, payload *adminmessages.OfficeUserUpdate, primaryTransportationOfficeId uuid.UUID) (*models.OfficeUser, *validate.Errors, error) } diff --git a/pkg/services/office_user/office_user_updater.go b/pkg/services/office_user/office_user_updater.go index 8eeb1326533..5d97152b0c1 100644 --- a/pkg/services/office_user/office_user_updater.go +++ b/pkg/services/office_user/office_user_updater.go @@ -16,10 +16,10 @@ type officeUserUpdater struct { } // UpdateOfficeUser updates an office user -func (o *officeUserUpdater) UpdateOfficeUser(appCtx appcontext.AppContext, id uuid.UUID, payload *adminmessages.OfficeUserUpdate) (*models.OfficeUser, *validate.Errors, error) { +func (o *officeUserUpdater) UpdateOfficeUser(appCtx appcontext.AppContext, id uuid.UUID, payload *adminmessages.OfficeUserUpdate, primaryTransportationOfficeID uuid.UUID) (*models.OfficeUser, *validate.Errors, error) { var foundUser models.OfficeUser - filters := []services.QueryFilter{query.NewQueryFilter("id", "=", id.String())} - err := o.builder.FetchOne(appCtx, &foundUser, filters) + officeUserFetcher := NewOfficeUserFetcherPop() + foundUser, err := officeUserFetcher.FetchOfficeUserByID(appCtx, id) if err != nil { return nil, nil, err @@ -46,8 +46,8 @@ func (o *officeUserUpdater) UpdateOfficeUser(appCtx appcontext.AppContext, id uu foundUser.Active = *payload.Active } - transportationOfficeID := payload.TransportationOfficeID.String() - if transportationOfficeID != uuid.Nil.String() && transportationOfficeID != "" { + transportationOfficeID := primaryTransportationOfficeID.String() + if primaryTransportationOfficeID != uuid.Nil && transportationOfficeID != uuid.Nil.String() && transportationOfficeID != "" { transportationIDFilter := []services.QueryFilter{ query.NewQueryFilter("id", "=", transportationOfficeID), } diff --git a/pkg/services/office_user/office_user_updater_test.go b/pkg/services/office_user/office_user_updater_test.go index a2a61114fc3..e1057af02cf 100644 --- a/pkg/services/office_user/office_user_updater_test.go +++ b/pkg/services/office_user/office_user_updater_test.go @@ -20,14 +20,20 @@ func (suite *OfficeUserServiceSuite) TestUpdateOfficeUser() { suite.Run("If the user is updated successfully it should be returned", func() { officeUser := factory.BuildOfficeUser(suite.DB(), nil, nil) transportationOffice := factory.BuildDefaultTransportationOffice(suite.DB()) + primaryOffice := true firstName := "Lea" payload := &adminmessages.OfficeUserUpdate{ - FirstName: &firstName, - TransportationOfficeID: strfmt.UUID(transportationOffice.ID.String()), + FirstName: &firstName, + TransportationOfficeAssignments: []*adminmessages.OfficeUserTransportationOfficeAssignment{ + { + TransportationOfficeID: strfmt.UUID(transportationOffice.ID.String()), + PrimaryOffice: &primaryOffice, + }, + }, } - updatedOfficeUser, verrs, err := updater.UpdateOfficeUser(suite.AppContextForTest(), officeUser.ID, payload) + updatedOfficeUser, verrs, err := updater.UpdateOfficeUser(suite.AppContextForTest(), officeUser.ID, payload, uuid.FromStringOrNil(transportationOffice.ID.String())) suite.NoError(err) suite.Nil(verrs) suite.Equal(updatedOfficeUser.ID.String(), officeUser.ID.String()) @@ -41,7 +47,7 @@ func (suite *OfficeUserServiceSuite) TestUpdateOfficeUser() { suite.Run("If we are provided an office user that doesn't exist, the create should fail", func() { payload := &adminmessages.OfficeUserUpdate{} - _, _, err := updater.UpdateOfficeUser(suite.AppContextForTest(), uuid.FromStringOrNil("00000000-0000-0000-0000-000000000001"), payload) + _, _, err := updater.UpdateOfficeUser(suite.AppContextForTest(), uuid.FromStringOrNil("00000000-0000-0000-0000-000000000001"), payload, uuid.Nil) suite.Error(err) suite.Equal(sql.ErrNoRows.Error(), err.Error()) }) @@ -56,11 +62,18 @@ func (suite *OfficeUserServiceSuite) TestUpdateOfficeUser() { }, }, }, nil) + primaryOffice := true + payload := &adminmessages.OfficeUserUpdate{ - TransportationOfficeID: strfmt.UUID("00000000-0000-0000-0000-000000000001"), + TransportationOfficeAssignments: []*adminmessages.OfficeUserTransportationOfficeAssignment{ + { + TransportationOfficeID: strfmt.UUID("00000000-0000-0000-0000-000000000001"), + PrimaryOffice: &primaryOffice, + }, + }, } - _, _, err := updater.UpdateOfficeUser(suite.AppContextForTest(), officeUser.ID, payload) + _, _, err := updater.UpdateOfficeUser(suite.AppContextForTest(), officeUser.ID, payload, uuid.FromStringOrNil("00000000-0000-0000-0000-000000000001")) suite.Error(err) suite.Equal(sql.ErrNoRows.Error(), err.Error()) }) diff --git a/pkg/services/transportation_office_assignments.go b/pkg/services/transportation_office_assignments.go new file mode 100644 index 00000000000..f4e4fd15b95 --- /dev/null +++ b/pkg/services/transportation_office_assignments.go @@ -0,0 +1,15 @@ +package services + +import ( + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" +) + +// TransportaionOfficeAssignmentUpdater is the service object interface for UpdateTransportaionOfficeAssignments +// +//go:generate mockery --name TransportaionOfficeAssignmentUpdater +type TransportaionOfficeAssignmentUpdater interface { + UpdateTransportaionOfficeAssignments(appCtx appcontext.AppContext, officeUserId uuid.UUID, transportationOfficeAssignments models.TransportationOfficeAssignments) (models.TransportationOfficeAssignments, error) +} diff --git a/pkg/services/transportation_office_assignments/transportation_office_assignments_updater.go b/pkg/services/transportation_office_assignments/transportation_office_assignments_updater.go new file mode 100644 index 00000000000..dc44b2e48f7 --- /dev/null +++ b/pkg/services/transportation_office_assignments/transportation_office_assignments_updater.go @@ -0,0 +1,57 @@ +package transportationofficeassignments + +import ( + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + // "github.com/transcom/mymove/pkg/db/utilities" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" +) + +type transportaionOfficeAssignmentUpdater struct { +} + +// NewTransportaionOfficeAssignmentUpdater creates a new struct with the service dependencies +func NewTransportaionOfficeAssignmentUpdater() services.TransportaionOfficeAssignmentUpdater { + return transportaionOfficeAssignmentUpdater{} +} + +func (updater transportaionOfficeAssignmentUpdater) UpdateTransportaionOfficeAssignments( + appCtx appcontext.AppContext, + officeUserId uuid.UUID, + newAssignments models.TransportationOfficeAssignments, +) (models.TransportationOfficeAssignments, error) { + + var existingAssignments models.TransportationOfficeAssignments + err := appCtx.DB().Where("id = ?", officeUserId).All(&existingAssignments) + + if err != nil { + return models.TransportationOfficeAssignments{}, err + } + + var assignmentsToCreate models.TransportationOfficeAssignments + + for _, newAssignment := range newAssignments { + newAssignment.ID = officeUserId + assignmentsToCreate = append(assignmentsToCreate, newAssignment) + } + + err = appCtx.DB().Destroy(existingAssignments) + if err != nil { + return models.TransportationOfficeAssignments{}, err + } + + err = appCtx.DB().Create(assignmentsToCreate) + if err != nil { + return models.TransportationOfficeAssignments{}, err + } + + var assignments models.TransportationOfficeAssignments + err = appCtx.DB().Where("id = ?", officeUserId).All(&assignments) + if err != nil { + return models.TransportationOfficeAssignments{}, err + } + + return assignments, nil +} diff --git a/pkg/services/user/user_updater.go b/pkg/services/user/user_updater.go index 7026cf096a7..d1c9985e4d8 100644 --- a/pkg/services/user/user_updater.go +++ b/pkg/services/user/user_updater.go @@ -75,7 +75,7 @@ func (o *userUpdater) UpdateUser(appCtx appcontext.AppContext, id uuid.UUID, use payload := adminmessages.OfficeUserUpdate{ Active: &user.Active, } - _, verrs, err = o.officeUserUpdater.UpdateOfficeUser(appCtx, foundOfficeUser.ID, &payload) + _, verrs, err = o.officeUserUpdater.UpdateOfficeUser(appCtx, foundOfficeUser.ID, &payload, uuid.Nil) if verrs != nil { appCtx.Logger().Error("Could not update office user", zap.Error(verrs)) diff --git a/pkg/services/users_privileges/users_privileges.go b/pkg/services/users_privileges/users_privileges.go index a4ff399803e..6e17556f291 100644 --- a/pkg/services/users_privileges/users_privileges.go +++ b/pkg/services/users_privileges/users_privileges.go @@ -51,8 +51,8 @@ func (u usersPrivilegesCreator) addUserPrivileges(appCtx appcontext.AppContext, // AND ur.user_id ISNULL; var userPrivilegesToAdd []models.UsersPrivileges if len(rs) > 0 { - err := appCtx.DB().Select("r.id as privilege_id, ? as user_id"). - RightJoin("privileges r", "r.id=users_privileges.privilege_id AND users_privileges.user_id = ? AND users_privileges.deleted_at IS NULL", userID, userID). + err := appCtx.DB().Select("p.id as privilege_id, ? as user_id"). + RightJoin("privileges p", "r.id=users_privileges.privilege_id AND users_privileges.user_id = ? AND users_privileges.deleted_at IS NULL", userID, userID). Where("privilege_type IN (?) AND (users_privileges.user_id IS NULL)", rs). All(&userPrivilegesToAdd) if err != nil { @@ -83,8 +83,8 @@ func (u usersPrivilegesCreator) removeUserPrivileges(appCtx appcontext.AppContex // AND ur.user_id IS NOT NULL; var userPrivilegesToDelete []models.UsersPrivileges if len(rs) > 0 { - err := appCtx.DB().Select("users_privileges.id, r.id as privilege_id, ? as user_id, users_privileges.deleted_at"). - RightJoin("privileges r", "r.id=users_privileges.privilege_id AND users_privileges.user_id = ? AND users_privileges.deleted_at IS NULL", userID, userID). + err := appCtx.DB().Select("users_privileges.id, p.id as privilege_id, ? as user_id, users_privileges.deleted_at"). + RightJoin("privileges p", "p.id=users_privileges.privilege_id AND users_privileges.user_id = ? AND users_privileges.deleted_at IS NULL", userID, userID). Where("privilege_type NOT IN (?) AND users_privileges.id IS NOT NULL", rs). All(&userPrivilegesToDelete) if err != nil { diff --git a/swagger-def/admin.yaml b/swagger-def/admin.yaml index 9eb2f4d55ef..a98b6fdd200 100644 --- a/swagger-def/admin.yaml +++ b/swagger-def/admin.yaml @@ -677,6 +677,10 @@ definitions: transportationOfficeId: type: string format: uuid + transportationOfficeAssignments: + type: array + items: + $ref: '#/definitions/TransportationOfficeAssignment' active: type: boolean roles: @@ -715,6 +719,7 @@ definitions: - email - telephone - transportationOfficeId + - transportationOfficeAssignments - active - roles - edipi @@ -746,10 +751,10 @@ definitions: format: telephone pattern: '^[2-9]\d{2}-\d{3}-\d{4}$' example: 212-555-5555 - transportationOfficeId: - type: string - format: uuid - example: "c56a4180-65aa-42ec-a945-5fd21dec0538" + transportationOfficeAssignments: + type: array + items: + $ref: '#/definitions/OfficeUserTransportationOfficeAssignment' roles: type: array items: @@ -787,11 +792,11 @@ definitions: roles: type: array items: - $ref: "#/definitions/OfficeUserRole" - transportationOfficeId: - type: string - format: uuid - example: "c56a4180-65aa-42ec-a945-5fd21dec0538" + $ref: '#/definitions/OfficeUserRole' + transportationOfficeAssignments: + type: array + items: + $ref: '#/definitions/OfficeUserTransportationOfficeAssignment' privileges: type: array items: @@ -822,6 +827,18 @@ definitions: example: "supervisor" x-nullable: true title: privilegeType + OfficeUserTransportationOfficeAssignment: + type: object + properties: + transportationOfficeId: + type: string + format: uuid + example: 'c56a4180-65aa-42ec-a945-5fd21dec0538' + title: transportationOfficeId + primaryOffice: + type: boolean + x-nullable: true + title: primaryOffice OfficeUsers: type: array items: @@ -958,6 +975,30 @@ definitions: type: string format: date-time readOnly: true + TransportationOfficeAssignment: + type: object + properties: + officeUserId: + type: string + format: uuid + example: c56a4780-65aa-42ec-a945-5fd87dec0538 + transportationOfficeId: + type: string + format: uuid + example: d67a4780-65aa-42ec-a945-5fd87dec0549 + transportationOffice: + $ref: '#/definitions/TransportationOffice' + primaryOffice: + type: boolean + x-omitempty: false + createdAt: + type: string + format: date-time + readOnly: true + updatedAt: + type: string + format: date-time + readOnly: true ServiceMember: type: object properties: diff --git a/swagger/admin.yaml b/swagger/admin.yaml index 3fa4aa66588..5a00ed3321c 100644 --- a/swagger/admin.yaml +++ b/swagger/admin.yaml @@ -684,6 +684,10 @@ definitions: transportationOfficeId: type: string format: uuid + transportationOfficeAssignments: + type: array + items: + $ref: '#/definitions/TransportationOfficeAssignment' active: type: boolean roles: @@ -722,6 +726,7 @@ definitions: - email - telephone - transportationOfficeId + - transportationOfficeAssignments - active - roles - edipi @@ -753,10 +758,10 @@ definitions: format: telephone pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ example: 212-555-5555 - transportationOfficeId: - type: string - format: uuid - example: c56a4180-65aa-42ec-a945-5fd21dec0538 + transportationOfficeAssignments: + type: array + items: + $ref: '#/definitions/OfficeUserTransportationOfficeAssignment' roles: type: array items: @@ -795,10 +800,10 @@ definitions: type: array items: $ref: '#/definitions/OfficeUserRole' - transportationOfficeId: - type: string - format: uuid - example: c56a4180-65aa-42ec-a945-5fd21dec0538 + transportationOfficeAssignments: + type: array + items: + $ref: '#/definitions/OfficeUserTransportationOfficeAssignment' privileges: type: array items: @@ -829,6 +834,18 @@ definitions: example: supervisor x-nullable: true title: privilegeType + OfficeUserTransportationOfficeAssignment: + type: object + properties: + transportationOfficeId: + type: string + format: uuid + example: c56a4180-65aa-42ec-a945-5fd21dec0538 + title: transportationOfficeId + primaryOffice: + type: boolean + x-nullable: true + title: primaryOffice OfficeUsers: type: array items: @@ -965,6 +982,30 @@ definitions: type: string format: date-time readOnly: true + TransportationOfficeAssignment: + type: object + properties: + officeUserId: + type: string + format: uuid + example: c56a4780-65aa-42ec-a945-5fd87dec0538 + transportationOfficeId: + type: string + format: uuid + example: d67a4780-65aa-42ec-a945-5fd87dec0549 + transportationOffice: + $ref: '#/definitions/TransportationOffice' + primaryOffice: + type: boolean + x-omitempty: false + createdAt: + type: string + format: date-time + readOnly: true + updatedAt: + type: string + format: date-time + readOnly: true ServiceMember: type: object properties: