1310 lines
39 KiB
Go
1310 lines
39 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"wucher/internal/domain/auth"
|
|
"wucher/internal/domain/base"
|
|
"wucher/internal/domain/contact"
|
|
"wucher/internal/shared/pkg/apperrors"
|
|
"wucher/internal/shared/pkg/sortkey"
|
|
)
|
|
|
|
type ContactRepository struct {
|
|
db *gorm.DB
|
|
hasBaseContactRolesTable bool
|
|
baseContactRolesHasContactID bool
|
|
baseContactRolesHasDeletedAt bool
|
|
}
|
|
|
|
const contactBaseRoleDUL = "dul"
|
|
|
|
const microsoftIdentityProvider = "microsoft"
|
|
|
|
var contactProfileTables = []string{
|
|
"pilot_profiles",
|
|
"doctor_profiles",
|
|
"air_rescuer_profiles",
|
|
"technician_profiles",
|
|
"flight_assistant_profiles",
|
|
"staff_profiles",
|
|
}
|
|
|
|
func NewContactRepository(db *gorm.DB) *ContactRepository {
|
|
schema := newSchemaCache(db)
|
|
hasBaseContactRolesTable := schema.HasTable("base_contact_roles")
|
|
return &ContactRepository{
|
|
db: db,
|
|
hasBaseContactRolesTable: hasBaseContactRolesTable,
|
|
baseContactRolesHasContactID: hasBaseContactRolesTable && schema.HasColumn("base_contact_roles", "contact_id"),
|
|
baseContactRolesHasDeletedAt: hasBaseContactRolesTable && schema.HasColumn("base_contact_roles", "deleted_at"),
|
|
}
|
|
}
|
|
|
|
func copyByteSlices(in [][]byte) [][]byte {
|
|
out := make([][]byte, 0, len(in))
|
|
for i := range in {
|
|
if len(in[i]) == 0 {
|
|
continue
|
|
}
|
|
out = append(out, append([]byte(nil), in[i]...))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *ContactRepository) ListContacts(ctx context.Context, filter contact.ListFilter) ([]contact.ContactListItem, int64, error) {
|
|
items := make([]contact.ContactListItem, 0)
|
|
var total int64
|
|
|
|
base := r.baseListQuery(ctx)
|
|
base = applyContactFilters(base, filter)
|
|
|
|
if err := base.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
query := base.Select(contactDetailSelectSQL())
|
|
if strings.TrimSpace(filter.Sort) != "" {
|
|
query = query.Order(filter.Sort)
|
|
} else {
|
|
for _, clause := range sortkey.ActivePositiveSortClauses("users", "is_active", "sortkey", "first_name", false) {
|
|
query = query.Order(clause)
|
|
}
|
|
}
|
|
|
|
if filter.Limit > 0 {
|
|
query = query.Limit(filter.Limit).Offset(filter.Offset)
|
|
}
|
|
|
|
if err := query.Scan(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (r *ContactRepository) GetContactByID(ctx context.Context, userID []byte) (*contact.ContactListItem, error) {
|
|
item := &contact.ContactListItem{}
|
|
q := r.baseListQuery(ctx).Where("users.id = ?", userID)
|
|
if err := q.Select(contactDetailSelectSQL()).Take(item).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func (r *ContactRepository) IsActorAdmin(ctx context.Context) (bool, error) {
|
|
return r.isActorAdmin(ctx, r.db.WithContext(ctx))
|
|
}
|
|
|
|
func (r *ContactRepository) ResolveRoleCodeByID(ctx context.Context, roleID []byte) (string, error) {
|
|
_, roleCode, err := r.resolveRoleByID(r.db.WithContext(ctx), roleID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return roleCode, nil
|
|
}
|
|
|
|
func (r *ContactRepository) CreateContact(ctx context.Context, in contact.CreateInput) ([]byte, error) {
|
|
var outID []byte
|
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if in.User.SSOEmail != nil {
|
|
isAdmin, err := r.isActorAdmin(ctx, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isAdmin {
|
|
return errors.New("only admin can set sso_email")
|
|
}
|
|
}
|
|
assignedRoleIDs := make([][]byte, 0, len(in.RoleIDs))
|
|
assignedRoleCodes := make([]string, 0, len(in.RoleIDs))
|
|
switch {
|
|
case len(in.RoleIDs) > 0:
|
|
for i := range in.RoleIDs {
|
|
resolvedID, resolvedCode, err := r.resolveRoleByID(tx, in.RoleIDs[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
assignedRoleIDs = append(assignedRoleIDs, resolvedID)
|
|
assignedRoleCodes = append(assignedRoleCodes, resolvedCode)
|
|
}
|
|
case len(in.RoleID) == 16:
|
|
resolvedID, resolvedCode, err := r.resolveRoleByID(tx, in.RoleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
assignedRoleIDs = append(assignedRoleIDs, resolvedID)
|
|
assignedRoleCodes = append(assignedRoleCodes, resolvedCode)
|
|
case len(in.Profiles) > 0:
|
|
seen := make(map[string]struct{}, len(in.Profiles))
|
|
for i := range in.Profiles {
|
|
resolvedID, resolvedCode, err := r.resolveRoleID(tx, in.Profiles[i].RoleCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key := string(resolvedID)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
assignedRoleIDs = append(assignedRoleIDs, resolvedID)
|
|
assignedRoleCodes = append(assignedRoleCodes, resolvedCode)
|
|
}
|
|
default:
|
|
// Role assignment is optional for contact creation.
|
|
}
|
|
var roleID []byte
|
|
normalizedRoleCode := ""
|
|
if len(assignedRoleIDs) > 0 {
|
|
roleID = assignedRoleIDs[0]
|
|
normalizedRoleCode = assignedRoleCodes[0]
|
|
}
|
|
var adminRoleIDs [][]byte
|
|
if in.User.IsAdmin != nil && *in.User.IsAdmin {
|
|
adminRoleID, _, err := r.resolveRoleID(tx, "admin")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
adminRoleIDs = append(adminRoleIDs, adminRoleID)
|
|
}
|
|
|
|
email := strings.TrimSpace(valueOrEmpty(in.User.Email))
|
|
ssoEmail := normalizedEmailPtr(in.User.SSOEmail)
|
|
firstName := strings.TrimSpace(valueOrEmpty(in.User.FirstName))
|
|
lastName := strings.TrimSpace(valueOrEmpty(in.User.LastName))
|
|
if email == "" || firstName == "" || lastName == "" {
|
|
return errors.New("email, first_name, and last_name are required")
|
|
}
|
|
if err := ensureUniqueSSOEmailTx(tx, ssoEmail, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
tz := "UTC"
|
|
if in.User.Timezone != nil && strings.TrimSpace(*in.User.Timezone) != "" {
|
|
tz = strings.TrimSpace(*in.User.Timezone)
|
|
}
|
|
isActive := true
|
|
if in.User.IsActive != nil {
|
|
isActive = *in.User.IsActive
|
|
}
|
|
username := trimmedPtr(in.User.Username)
|
|
if username != nil {
|
|
exists, err := usernameExistsTx(tx, *username, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return contact.ErrUsernameAlreadyExists
|
|
}
|
|
}
|
|
|
|
u := &auth.User{
|
|
Email: email,
|
|
SSOEmail: ssoEmail,
|
|
Username: username,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
MobilePhone: strings.TrimSpace(valueOrEmpty(in.User.MobilePhone)),
|
|
Timezone: tz,
|
|
SortKey: in.User.SortKey,
|
|
RoleID: roleID,
|
|
RoleIDs: append(copyByteSlices(assignedRoleIDs), adminRoleIDs...),
|
|
IsActive: isActive,
|
|
}
|
|
if in.User.ProfileAttachmentID != nil {
|
|
u.ProfileAttachmentID = append([]byte(nil), (*in.User.ProfileAttachmentID)...)
|
|
}
|
|
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
|
|
u.CreatedBy = actor
|
|
u.UpdatedBy = actor
|
|
}
|
|
if err := tx.Create(u).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&auth.User{}).Where("id = ?", u.ID).UpdateColumn("is_active", isActive).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs); err != nil {
|
|
return err
|
|
}
|
|
if len(in.Profiles) > 0 {
|
|
for i := range in.Profiles {
|
|
if err := r.upsertProfileTx(ctx, tx, u.ID, in.Profiles[i].RoleCode, in.Profiles[i].Patch, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if normalizedRoleCode != "" && hasAnyProfilePatch(in.Profile) {
|
|
if err := r.upsertProfileTx(ctx, tx, u.ID, normalizedRoleCode, in.Profile, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
outID = append([]byte(nil), u.ID...)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return outID, nil
|
|
}
|
|
|
|
func (r *ContactRepository) UpdateContact(ctx context.Context, in contact.UpdateInput) error {
|
|
if len(in.UserID) != 16 {
|
|
return errors.New("invalid user id")
|
|
}
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
exists, err := r.userExistsTx(tx, in.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
|
|
updates := map[string]any{}
|
|
ssoEmailUpdated := false
|
|
ssoEmailCleared := false
|
|
if in.User.SSOEmail != nil {
|
|
isAdmin, err := r.isActorAdmin(ctx, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isAdmin {
|
|
return errors.New("only admin can update sso_email")
|
|
}
|
|
normalizedSSO := normalizedEmailPtr(in.User.SSOEmail)
|
|
if err := ensureUniqueSSOEmailTx(tx, normalizedSSO, in.UserID); err != nil {
|
|
return err
|
|
}
|
|
updates["sso_email"] = normalizedSSO
|
|
ssoEmailUpdated = true
|
|
ssoEmailCleared = normalizedSSO == nil
|
|
}
|
|
if in.User.Email != nil {
|
|
updates["email"] = strings.TrimSpace(*in.User.Email)
|
|
}
|
|
if in.User.Username != nil {
|
|
username := trimmedPtr(in.User.Username)
|
|
if username != nil {
|
|
exists, err := usernameExistsTx(tx, *username, in.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return contact.ErrUsernameAlreadyExists
|
|
}
|
|
}
|
|
updates["username"] = username
|
|
}
|
|
if in.User.FirstName != nil {
|
|
updates["first_name"] = strings.TrimSpace(*in.User.FirstName)
|
|
}
|
|
if in.User.LastName != nil {
|
|
updates["last_name"] = strings.TrimSpace(*in.User.LastName)
|
|
}
|
|
if in.User.MobilePhone != nil {
|
|
updates["mobile_phone"] = strings.TrimSpace(*in.User.MobilePhone)
|
|
}
|
|
if in.User.Timezone != nil {
|
|
updates["timezone"] = strings.TrimSpace(*in.User.Timezone)
|
|
}
|
|
if in.User.SortKeySet {
|
|
if in.User.SortKey == nil {
|
|
updates["sortkey"] = nil
|
|
} else {
|
|
updates["sortkey"] = *in.User.SortKey
|
|
}
|
|
}
|
|
if in.User.IsActive != nil {
|
|
updates["is_active"] = *in.User.IsActive
|
|
}
|
|
if in.User.ProfileAttachmentID != nil {
|
|
updates["profile_attachment_id"] = *in.User.ProfileAttachmentID
|
|
}
|
|
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
|
|
updates["updated_by"] = actor
|
|
}
|
|
if len(updates) > 0 {
|
|
if err := tx.Table("users").Where("id = ?", in.UserID).Updates(updates).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if ssoEmailUpdated && ssoEmailCleared {
|
|
if err := tx.Where("user_id = ? AND provider = ?", in.UserID, microsoftIdentityProvider).
|
|
Delete(&auth.UserIdentity{}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if in.UpdateRoles {
|
|
if len(in.RoleID) != 0 && len(in.RoleID) != 16 {
|
|
return errors.New("invalid role_id")
|
|
}
|
|
if err := tx.Table("users").Where("id = ?", in.UserID).Update("role_id", in.RoleID).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := replaceUserRolesTx(ctx, tx, in.UserID, in.RoleID, in.RoleIDs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if in.User.IsAdmin != nil {
|
|
if err := r.syncAdminRoleTx(ctx, tx, in.UserID, *in.User.IsAdmin); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(in.Profiles) > 0 {
|
|
for i := range in.Profiles {
|
|
if err := r.upsertProfileTx(ctx, tx, in.UserID, in.Profiles[i].RoleCode, in.Profiles[i].Patch, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := r.touchUserUpdatedByTx(ctx, tx, in.UserID); err != nil {
|
|
return err
|
|
}
|
|
} else if hasAnyProfilePatch(in.Profile) {
|
|
roleCode := strings.TrimSpace(in.TargetProfileRole)
|
|
if roleCode == "" {
|
|
resolvedRoleCode, err := r.roleCodeForUser(tx, in.UserID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// Contact can now have no role. If profile payload still comes from FE
|
|
// while no role is assigned, skip profile upsert and keep user update successful.
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
roleCode = resolvedRoleCode
|
|
}
|
|
if err := r.upsertProfileTx(ctx, tx, in.UserID, roleCode, in.Profile, false); err != nil {
|
|
return err
|
|
}
|
|
if err := r.touchUserUpdatedByTx(ctx, tx, in.UserID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) DeleteContact(ctx context.Context, userID []byte) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
exists, err := r.userExistsTx(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
if r.hasBaseContactRolesTable && r.baseContactRolesHasContactID {
|
|
query := tx.Table("base_contact_roles").Where("contact_id = ?", userID)
|
|
if r.baseContactRolesHasDeletedAt {
|
|
query = query.Where("deleted_at IS NULL")
|
|
}
|
|
var count int64
|
|
if err := query.Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return apperrors.NewDeleteConflictError("bases")
|
|
}
|
|
}
|
|
if err := r.deleteContactProfilesTx(tx, userID); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("user_id = ?", userID).Delete(&auth.UserRole{}).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Where("id = ?", userID).Delete(&auth.User{}).Error
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) UpdateContactStatus(ctx context.Context, userID []byte, isActive bool) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
exists, err := r.userExistsTx(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
updates := map[string]any{"is_active": isActive}
|
|
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
|
|
updates["updated_by"] = actor
|
|
}
|
|
return tx.Table("users").Where("id = ?", userID).Updates(updates).Error
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) UpdateContactRole(ctx context.Context, userID []byte, roleCode string) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
exists, err := r.userExistsTx(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
|
|
currentRoleID, err := r.primaryRoleIDForUserTx(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
roleID, normalizedRoleCode, err := r.resolveRoleID(tx, roleCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
userUpdates := map[string]any{"role_id": roleID}
|
|
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
|
|
userUpdates["updated_by"] = actor
|
|
}
|
|
if err := tx.Table("users").Where("id = ?", userID).Updates(userUpdates).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := replacePrimaryUserRoleTx(ctx, tx, userID, currentRoleID, roleID); err != nil {
|
|
return err
|
|
}
|
|
// Ensure profile row exists for the new role.
|
|
return r.upsertProfileTx(ctx, tx, userID, normalizedRoleCode, contact.ProfilePatch{}, false)
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) userExistsTx(tx *gorm.DB, userID []byte) (bool, error) {
|
|
var count int64
|
|
if err := tx.Table("users").Where("id = ?", userID).Limit(1).Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (r *ContactRepository) deleteContactProfilesTx(tx *gorm.DB, userID []byte) error {
|
|
for i := range contactProfileTables {
|
|
if err := tx.Exec("DELETE FROM "+contactProfileTables[i]+" WHERE user_id = ?", userID).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ContactRepository) UpdatePilotCategory(ctx context.Context, userID []byte, category string) error {
|
|
category = strings.ToLower(strings.TrimSpace(category))
|
|
if category == "" {
|
|
return errors.New("pilot_category is required")
|
|
}
|
|
patch := contact.ProfilePatch{PilotCategory: &category}
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := r.upsertProfileTx(ctx, tx, userID, auth.RoleCodePilot, patch, false); err != nil {
|
|
return err
|
|
}
|
|
return r.touchUserUpdatedByTx(ctx, tx, userID)
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) UpdateLicense(ctx context.Context, userID []byte, licenseNo *string) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
roleCode, err := r.roleCodeForUser(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
patch := contact.ProfilePatch{LicenseNo: licenseNo}
|
|
if err := r.upsertProfileTx(ctx, tx, userID, roleCode, patch, false); err != nil {
|
|
return err
|
|
}
|
|
return r.touchUserUpdatedByTx(ctx, tx, userID)
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) UpdateChiefFlags(ctx context.Context, userID []byte, patch contact.ProfilePatch) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
roleCode, err := r.roleCodeForUser(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := r.upsertProfileTx(ctx, tx, userID, roleCode, patch, false); err != nil {
|
|
return err
|
|
}
|
|
return r.touchUserUpdatedByTx(ctx, tx, userID)
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) UpdateResponsibleComplaints(ctx context.Context, userID []byte, value bool) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
patch := contact.ProfilePatch{IsResponsibleComplaints: &value}
|
|
if err := r.upsertProfileTx(ctx, tx, userID, auth.RoleCodeTechnician, patch, false); err != nil {
|
|
return err
|
|
}
|
|
return r.touchUserUpdatedByTx(ctx, tx, userID)
|
|
})
|
|
}
|
|
|
|
func (r *ContactRepository) BulkUpdateStatus(ctx context.Context, userIDs [][]byte, isActive bool) (int64, error) {
|
|
if len(userIDs) == 0 {
|
|
return 0, nil
|
|
}
|
|
updates := map[string]any{"is_active": isActive}
|
|
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
|
|
updates["updated_by"] = actor
|
|
}
|
|
res := r.db.WithContext(ctx).Table("users").Where("id IN ?", userIDs).Updates(updates)
|
|
if res.Error != nil {
|
|
return 0, res.Error
|
|
}
|
|
return res.RowsAffected, nil
|
|
}
|
|
|
|
func (r *ContactRepository) touchUserUpdatedByTx(ctx context.Context, tx *gorm.DB, userID []byte) error {
|
|
actor := actorUserIDFromContext(ctx)
|
|
if len(actor) == 0 {
|
|
return nil
|
|
}
|
|
return tx.Table("users").Where("id = ?", userID).Update("updated_by", actor).Error
|
|
}
|
|
|
|
func (r *ContactRepository) baseListQuery(ctx context.Context) *gorm.DB {
|
|
return r.db.WithContext(ctx).
|
|
Table("users").
|
|
Joins("LEFT JOIN roles ON roles.id = users.role_id").
|
|
Joins("LEFT JOIN pilot_profiles pp ON pp.user_id = users.id").
|
|
Joins("LEFT JOIN doctor_profiles dp ON dp.user_id = users.id").
|
|
Joins("LEFT JOIN air_rescuer_profiles arp ON arp.user_id = users.id").
|
|
Joins("LEFT JOIN technician_profiles tp ON tp.user_id = users.id").
|
|
Joins("LEFT JOIN flight_assistant_profiles fap ON fap.user_id = users.id").
|
|
Joins("LEFT JOIN staff_profiles sp ON sp.user_id = users.id")
|
|
}
|
|
|
|
func (r *ContactRepository) isActorAdmin(ctx context.Context, tx *gorm.DB) (bool, error) {
|
|
actor := actorUserIDFromContext(ctx)
|
|
if len(actor) != 16 {
|
|
return false, nil
|
|
}
|
|
var count int64
|
|
err := tx.Table("users AS actor_users").
|
|
Joins("LEFT JOIN user_roles ur ON ur.user_id = actor_users.id").
|
|
Joins("LEFT JOIN roles role_primary ON role_primary.id = actor_users.role_id").
|
|
Joins("LEFT JOIN roles role_assigned ON role_assigned.id = ur.role_id").
|
|
Where("actor_users.id = ?", actor).
|
|
Where(`(
|
|
LOWER(COALESCE(NULLIF(role_primary.code, ''), role_primary.name)) = ? OR
|
|
LOWER(COALESCE(NULLIF(role_assigned.code, ''), role_assigned.name)) = ?
|
|
)`, "admin", "admin").
|
|
Count(&count).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func applyContactFilters(q *gorm.DB, filter contact.ListFilter) *gorm.DB {
|
|
role := strings.ToLower(strings.TrimSpace(filter.Role))
|
|
if role != "" {
|
|
q = q.Where(`(
|
|
LOWER(COALESCE(NULLIF(roles.code, ''), roles.name)) = ?
|
|
OR
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM user_roles ur_filter
|
|
JOIN roles r_filter ON r_filter.id = ur_filter.role_id
|
|
WHERE ur_filter.user_id = users.id
|
|
AND LOWER(COALESCE(NULLIF(r_filter.code, ''), r_filter.name)) = ?
|
|
)
|
|
)`, role, role)
|
|
}
|
|
|
|
pilotCategory := strings.ToLower(strings.TrimSpace(filter.PilotCategory))
|
|
if pilotCategory != "" {
|
|
q = q.Where(`(
|
|
LOWER(COALESCE(NULLIF(roles.code, ''), roles.name)) = ?
|
|
OR
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM user_roles ur_pilot
|
|
JOIN roles r_pilot ON r_pilot.id = ur_pilot.role_id
|
|
WHERE ur_pilot.user_id = users.id
|
|
AND LOWER(COALESCE(NULLIF(r_pilot.code, ''), r_pilot.name)) = ?
|
|
)
|
|
)`, auth.RoleCodePilot, auth.RoleCodePilot).Where("pp.pilot_category = ?", pilotCategory)
|
|
}
|
|
if filter.ChiefDoctor != nil {
|
|
q = q.Where("COALESCE(dp.is_chief_physician, 0) = ?", *filter.ChiefDoctor)
|
|
}
|
|
if filter.ChiefTechnician != nil {
|
|
q = q.Where("COALESCE(tp.is_chief_technician, 0) = ?", *filter.ChiefTechnician)
|
|
}
|
|
if filter.ResponsibleComplaint != nil {
|
|
q = q.Where("COALESCE(tp.is_responsible_complaints, 0) = ?", *filter.ResponsibleComplaint)
|
|
}
|
|
if filter.ChiefPilot != nil {
|
|
q = q.Where("COALESCE(pp.is_chief_pilot, 0) = ?", *filter.ChiefPilot)
|
|
}
|
|
if filter.IFRQualified != nil {
|
|
q = q.Where("COALESCE(pp.has_ifr_qualification, 0) = ?", *filter.IFRQualified)
|
|
}
|
|
|
|
search := strings.TrimSpace(filter.Search)
|
|
if search != "" {
|
|
like := "%" + search + "%"
|
|
q = q.Where(`
|
|
users.username LIKE ? OR
|
|
users.email LIKE ? OR
|
|
users.first_name LIKE ? OR
|
|
users.last_name LIKE ? OR
|
|
pp.note LIKE ? OR
|
|
dp.note LIKE ? OR
|
|
arp.note LIKE ? OR
|
|
tp.note LIKE ? OR
|
|
fap.note LIKE ? OR
|
|
sp.note LIKE ? OR
|
|
pp.short_name LIKE ? OR
|
|
dp.short_name LIKE ? OR
|
|
arp.short_name LIKE ? OR
|
|
tp.short_name LIKE ? OR
|
|
fap.short_name LIKE ? OR
|
|
sp.short_name LIKE ?
|
|
`, like, like, like, like, like, like, like, like, like, like, like, like, like, like, like, like)
|
|
}
|
|
return q
|
|
}
|
|
|
|
func contactListSelectSQL() string {
|
|
return `
|
|
users.id AS user_id,
|
|
users.created_by AS created_by,
|
|
users.updated_by AS updated_by,
|
|
COALESCE(NULLIF(roles.code, ''), roles.name) AS role_code,
|
|
roles.name AS role_name,
|
|
pp.pilot_category AS pilot_category,
|
|
users.username AS username,
|
|
users.email AS email,
|
|
users.sso_email AS sso_email,
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM user_roles ur_admin
|
|
JOIN roles r_admin ON r_admin.id = ur_admin.role_id
|
|
WHERE ur_admin.user_id = users.id
|
|
AND LOWER(COALESCE(NULLIF(r_admin.code, ''), r_admin.name)) = 'admin'
|
|
) AS is_admin,
|
|
users.first_name AS first_name,
|
|
users.last_name AS last_name,
|
|
COALESCE(pp.short_name, dp.short_name, arp.short_name, tp.short_name, fap.short_name, sp.short_name) AS short_name,
|
|
COALESCE(pp.license_no, tp.license_no) AS license_no,
|
|
COALESCE(pp.location, dp.location, arp.location, tp.location, fap.location, sp.location) AS location,
|
|
users.mobile_phone AS mobile_phone,
|
|
users.profile_attachment_id AS profile_attachment_id,
|
|
(COALESCE(OCTET_LENGTH(users.password_hash), 0) > 0) AS has_password,
|
|
users.sortkey AS sortkey,
|
|
COALESCE(pp.note, dp.note, arp.note, tp.note, fap.note, sp.note, '') AS note,
|
|
users.is_active AS is_active,
|
|
users.created_at AS created_at,
|
|
users.updated_at AS updated_at
|
|
`
|
|
}
|
|
|
|
func contactDetailSelectSQL() string {
|
|
return `
|
|
users.id AS user_id,
|
|
users.created_by AS created_by,
|
|
users.updated_by AS updated_by,
|
|
COALESCE(NULLIF(roles.code, ''), roles.name) AS role_code,
|
|
roles.name AS role_name,
|
|
pp.pilot_category AS pilot_category,
|
|
users.username AS username,
|
|
users.email AS email,
|
|
users.sso_email AS sso_email,
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM user_roles ur_admin
|
|
JOIN roles r_admin ON r_admin.id = ur_admin.role_id
|
|
WHERE ur_admin.user_id = users.id
|
|
AND LOWER(COALESCE(NULLIF(r_admin.code, ''), r_admin.name)) = 'admin'
|
|
) AS is_admin,
|
|
users.first_name AS first_name,
|
|
users.last_name AS last_name,
|
|
COALESCE(pp.short_name, dp.short_name, arp.short_name, tp.short_name, fap.short_name, sp.short_name) AS short_name,
|
|
COALESCE(pp.license_no, tp.license_no) AS license_no,
|
|
COALESCE(pp.location, dp.location, arp.location, tp.location, fap.location, sp.location) AS location,
|
|
users.mobile_phone AS mobile_phone,
|
|
users.profile_attachment_id AS profile_attachment_id,
|
|
(COALESCE(OCTET_LENGTH(users.password_hash), 0) > 0) AS has_password,
|
|
users.sortkey AS sortkey,
|
|
COALESCE(pp.note, dp.note, arp.note, tp.note, fap.note, sp.note, '') AS note,
|
|
(
|
|
SELECT GROUP_CONCAT(CONCAT(LOWER(HEX(bcr.base_id)), '|', LOWER(HEX(b.base)), '|', bcr.role_code, '|', LOWER(HEX(COALESCE(bc.name, '')))))
|
|
FROM base_contact_roles bcr
|
|
JOIN bases b ON b.id = bcr.base_id AND b.deleted_at IS NULL
|
|
LEFT JOIN base_categories bc ON bc.id = b.base_category_id
|
|
WHERE bcr.contact_id = users.id
|
|
) AS base_roles_raw,
|
|
(
|
|
SELECT GROUP_CONCAT(CONCAT(LOWER(HEX(r_all.id)), '|', LOWER(HEX(r_all.name)), '|', LOWER(COALESCE(NULLIF(r_all.code, ''), r_all.name))))
|
|
FROM user_roles ur_all
|
|
JOIN roles r_all ON r_all.id = ur_all.role_id
|
|
WHERE ur_all.user_id = users.id
|
|
) AS roles_raw,
|
|
users.is_active AS is_active,
|
|
users.created_at AS created_at,
|
|
users.updated_at AS updated_at,
|
|
pp.short_name AS pilot_short_name,
|
|
pp.license_no AS pilot_license_no,
|
|
pp.license_issued_at AS pilot_license_issued_at,
|
|
pp.license_expired_at AS pilot_license_expired_at,
|
|
pp.technician_license_no AS pilot_technician_license_no,
|
|
pp.has_ifr_qualification AS pilot_has_ifr_qualification,
|
|
pp.is_chief_pilot AS pilot_is_chief_pilot,
|
|
(
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM base_contact_roles bcr_dul
|
|
JOIN bases b_dul ON b_dul.id = bcr_dul.base_id AND b_dul.deleted_at IS NULL
|
|
WHERE bcr_dul.contact_id = users.id
|
|
AND LOWER(bcr_dul.role_code) = 'dul'
|
|
AND b_dul.dul = 1
|
|
)
|
|
) AS pilot_is_dul,
|
|
pp.total_flight_minutes AS pilot_total_flight_minutes,
|
|
pp.responsible_pilot_minutes AS pilot_responsible_pilot_minutes,
|
|
pp.second_pilot_minutes AS pilot_second_pilot_minutes,
|
|
pp.double_command_minutes AS pilot_double_command_minutes,
|
|
pp.flight_instructor_minutes AS pilot_flight_instructor_minutes,
|
|
pp.night_flight_minutes AS pilot_night_flight_minutes,
|
|
pp.ifr_flight_minutes AS pilot_ifr_flight_minutes,
|
|
pp.location AS pilot_location,
|
|
pp.postcode AS pilot_postcode,
|
|
pp.street_line AS pilot_street_line,
|
|
pp.note AS pilot_note,
|
|
pp.weight_kg AS pilot_weight_kg,
|
|
dp.short_name AS doctor_short_name,
|
|
dp.specialization AS doctor_specialization,
|
|
dp.sub_specialization AS doctor_sub_specialization,
|
|
dp.is_chief_physician AS doctor_is_chief_physician,
|
|
dp.location AS doctor_location,
|
|
dp.postcode AS doctor_postcode,
|
|
dp.street_line AS doctor_street_line,
|
|
dp.note AS doctor_note,
|
|
arp.short_name AS air_rescuer_short_name,
|
|
arp.responsible_flight_rescuer AS air_rescuer_responsible_flight_rescuer,
|
|
arp.location AS air_rescuer_location,
|
|
arp.postcode AS air_rescuer_postcode,
|
|
arp.street_line AS air_rescuer_street_line,
|
|
arp.note AS air_rescuer_note,
|
|
tp.short_name AS technician_short_name,
|
|
tp.license_no AS technician_license_no,
|
|
tp.is_chief_technician AS technician_is_chief_technician,
|
|
tp.is_responsible_complaints AS technician_is_responsible_complaints,
|
|
tp.location AS technician_location,
|
|
tp.postcode AS technician_postcode,
|
|
tp.street_line AS technician_street_line,
|
|
tp.note AS technician_note,
|
|
fap.short_name AS flight_assistant_short_name,
|
|
fap.location AS flight_assistant_location,
|
|
fap.postcode AS flight_assistant_postcode,
|
|
fap.street_line AS flight_assistant_street_line,
|
|
fap.note AS flight_assistant_note,
|
|
sp.short_name AS staff_short_name,
|
|
sp.role_label AS staff_role_label,
|
|
sp.location AS staff_location,
|
|
sp.postcode AS staff_postcode,
|
|
sp.street_line AS staff_street_line,
|
|
sp.note AS staff_note
|
|
`
|
|
}
|
|
|
|
func (r *ContactRepository) roleCodeForUser(tx *gorm.DB, userID []byte) (string, error) {
|
|
var out struct{ RoleCode string }
|
|
err := tx.Table("users").
|
|
Select("LOWER(COALESCE(NULLIF(roles.code, ''), roles.name)) AS role_code").
|
|
Joins("JOIN roles ON roles.id = users.role_id").
|
|
Where("users.id = ?", userID).
|
|
Take(&out).Error
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(out.RoleCode), nil
|
|
}
|
|
|
|
func (r *ContactRepository) resolveRoleID(tx *gorm.DB, roleCode string) ([]byte, string, error) {
|
|
roleCode = strings.ToLower(strings.TrimSpace(roleCode))
|
|
if roleCode == "" {
|
|
return nil, "", errors.New("role is required")
|
|
}
|
|
var role auth.Role
|
|
err := tx.Where("LOWER(code) = ? OR LOWER(name) = ?", roleCode, roleCode).Take(&role).Error
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
normalized := strings.ToLower(strings.TrimSpace(role.Code))
|
|
if normalized == "" {
|
|
normalized = strings.ToLower(strings.TrimSpace(role.Name))
|
|
}
|
|
return role.ID, normalized, nil
|
|
}
|
|
|
|
func (r *ContactRepository) resolveRoleByID(tx *gorm.DB, roleID []byte) ([]byte, string, error) {
|
|
if len(roleID) != 16 {
|
|
return nil, "", errors.New("role_id is invalid")
|
|
}
|
|
var role auth.Role
|
|
if err := tx.Where("id = ?", roleID).Take(&role).Error; err != nil {
|
|
return nil, "", err
|
|
}
|
|
normalized := strings.ToLower(strings.TrimSpace(role.Code))
|
|
if normalized == "" {
|
|
normalized = strings.ToLower(strings.TrimSpace(role.Name))
|
|
}
|
|
return role.ID, normalized, nil
|
|
}
|
|
|
|
func (r *ContactRepository) upsertProfileTx(ctx context.Context, tx *gorm.DB, userID []byte, roleCode string, patch contact.ProfilePatch, isCreate bool) error {
|
|
now := time.Now().UTC()
|
|
actor := actorUserIDFromContext(ctx)
|
|
|
|
switch strings.ToLower(strings.TrimSpace(roleCode)) {
|
|
case auth.RoleCodePilot:
|
|
updates := map[string]any{}
|
|
if patch.PilotCategory != nil {
|
|
updates["pilot_category"] = strings.ToLower(strings.TrimSpace(*patch.PilotCategory))
|
|
}
|
|
if v := patch.ShortName; v != nil {
|
|
updates["short_name"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.LicenseNo; v != nil {
|
|
updates["license_no"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.LicenseIssuedAt; v != nil {
|
|
updates["license_issued_at"] = v
|
|
}
|
|
if v := patch.LicenseExpiredAt; v != nil {
|
|
updates["license_expired_at"] = v
|
|
}
|
|
if v := patch.TechnicianLicenseNo; v != nil {
|
|
updates["technician_license_no"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.HasIFRQualification; v != nil {
|
|
updates["has_ifr_qualification"] = *v
|
|
}
|
|
if v := patch.IsChiefPilot; v != nil {
|
|
updates["is_chief_pilot"] = *v
|
|
}
|
|
if v := patch.TotalFlightMinutes; v != nil {
|
|
updates["total_flight_minutes"] = *v
|
|
}
|
|
if v := patch.ResponsiblePilotMinutes; v != nil {
|
|
updates["responsible_pilot_minutes"] = *v
|
|
}
|
|
if v := patch.SecondPilotMinutes; v != nil {
|
|
updates["second_pilot_minutes"] = *v
|
|
}
|
|
if v := patch.DoubleCommandMinutes; v != nil {
|
|
updates["double_command_minutes"] = *v
|
|
}
|
|
if v := patch.FlightInstructorMinutes; v != nil {
|
|
updates["flight_instructor_minutes"] = *v
|
|
}
|
|
if v := patch.NightFlightMinutes; v != nil {
|
|
updates["night_flight_minutes"] = *v
|
|
}
|
|
if v := patch.IFRFlightMinutes; v != nil {
|
|
updates["ifr_flight_minutes"] = *v
|
|
}
|
|
if v := patch.Location; v != nil {
|
|
updates["location"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Postcode; v != nil {
|
|
updates["postcode"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.StreetLine; v != nil {
|
|
updates["street_line"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Note; v != nil {
|
|
updates["note"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.WeightKG; v != nil {
|
|
updates["weight_kg"] = *v
|
|
}
|
|
if isCreate && patch.PilotCategory == nil {
|
|
updates["pilot_category"] = auth.PilotCategoryRegular
|
|
}
|
|
if err := r.upsertByUserID(tx, "pilot_profiles", userID, updates, actor, now); err != nil {
|
|
return err
|
|
}
|
|
if patch.DULBaseIDsSet {
|
|
if err := r.syncContactDULBasesTx(ctx, tx, userID, patch.DULBaseIDs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
|
|
case auth.RoleCodeDoctor:
|
|
updates := map[string]any{}
|
|
if v := patch.ShortName; v != nil {
|
|
updates["short_name"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Specialization; v != nil {
|
|
updates["specialization"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.SubSpecialization; v != nil {
|
|
updates["sub_specialization"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.IsChiefPhysician; v != nil {
|
|
updates["is_chief_physician"] = *v
|
|
}
|
|
if v := patch.Location; v != nil {
|
|
updates["location"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Postcode; v != nil {
|
|
updates["postcode"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.StreetLine; v != nil {
|
|
updates["street_line"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Note; v != nil {
|
|
updates["note"] = strings.TrimSpace(*v)
|
|
}
|
|
return r.upsertByUserID(tx, "doctor_profiles", userID, updates, actor, now)
|
|
|
|
case auth.RoleCodeAirRescuer:
|
|
updates := map[string]any{}
|
|
if v := patch.ShortName; v != nil {
|
|
updates["short_name"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.ResponsibleFlightRescuer; v != nil {
|
|
updates["responsible_flight_rescuer"] = *v
|
|
}
|
|
if v := patch.Location; v != nil {
|
|
updates["location"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Postcode; v != nil {
|
|
updates["postcode"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.StreetLine; v != nil {
|
|
updates["street_line"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Note; v != nil {
|
|
updates["note"] = strings.TrimSpace(*v)
|
|
}
|
|
return r.upsertByUserID(tx, "air_rescuer_profiles", userID, updates, actor, now)
|
|
|
|
case auth.RoleCodeTechnician:
|
|
updates := map[string]any{}
|
|
if v := patch.ShortName; v != nil {
|
|
updates["short_name"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.LicenseNo; v != nil {
|
|
updates["license_no"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.IsChiefTechnician; v != nil {
|
|
updates["is_chief_technician"] = *v
|
|
}
|
|
if v := patch.IsResponsibleComplaints; v != nil {
|
|
updates["is_responsible_complaints"] = *v
|
|
}
|
|
if v := patch.Location; v != nil {
|
|
updates["location"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Postcode; v != nil {
|
|
updates["postcode"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.StreetLine; v != nil {
|
|
updates["street_line"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Note; v != nil {
|
|
updates["note"] = strings.TrimSpace(*v)
|
|
}
|
|
return r.upsertByUserID(tx, "technician_profiles", userID, updates, actor, now)
|
|
|
|
case auth.RoleCodeFlightAssistant:
|
|
updates := map[string]any{}
|
|
if v := patch.ShortName; v != nil {
|
|
updates["short_name"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Location; v != nil {
|
|
updates["location"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Postcode; v != nil {
|
|
updates["postcode"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.StreetLine; v != nil {
|
|
updates["street_line"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Note; v != nil {
|
|
updates["note"] = strings.TrimSpace(*v)
|
|
}
|
|
return r.upsertByUserID(tx, "flight_assistant_profiles", userID, updates, actor, now)
|
|
|
|
case auth.RoleCodeStaff:
|
|
updates := map[string]any{}
|
|
if v := patch.ShortName; v != nil {
|
|
updates["short_name"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.RoleLabel; v != nil {
|
|
updates["role_label"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Location; v != nil {
|
|
updates["location"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Postcode; v != nil {
|
|
updates["postcode"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.StreetLine; v != nil {
|
|
updates["street_line"] = strings.TrimSpace(*v)
|
|
}
|
|
if v := patch.Note; v != nil {
|
|
updates["note"] = strings.TrimSpace(*v)
|
|
}
|
|
return r.upsertByUserID(tx, "staff_profiles", userID, updates, actor, now)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ContactRepository) upsertByUserID(tx *gorm.DB, table string, userID []byte, updates map[string]any, actor []byte, now time.Time) error {
|
|
if updates == nil {
|
|
updates = map[string]any{}
|
|
}
|
|
exists, err := r.profileExistsTx(tx, table, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updates["updated_at"] = now
|
|
if len(actor) > 0 {
|
|
updates["updated_by"] = actor
|
|
}
|
|
if exists {
|
|
return tx.Table(table).Where("user_id = ?", userID).Updates(updates).Error
|
|
}
|
|
|
|
insert := map[string]any{"user_id": userID, "created_at": now, "updated_at": now}
|
|
if len(actor) > 0 {
|
|
insert["created_by"] = actor
|
|
insert["updated_by"] = actor
|
|
}
|
|
for k, v := range updates {
|
|
if k == "updated_at" || k == "updated_by" {
|
|
continue
|
|
}
|
|
insert[k] = v
|
|
}
|
|
return tx.Table(table).Create(insert).Error
|
|
}
|
|
|
|
func (r *ContactRepository) profileExistsTx(tx *gorm.DB, table string, userID []byte) (bool, error) {
|
|
var count int64
|
|
if err := tx.Table(table).Where("user_id = ?", userID).Limit(1).Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func valueOrEmpty(v *string) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func trimmedPtr(v *string) *string {
|
|
trimmed := strings.TrimSpace(valueOrEmpty(v))
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func normalizedEmailPtr(v *string) *string {
|
|
trimmed := strings.ToLower(strings.TrimSpace(valueOrEmpty(v)))
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func ensureUniqueSSOEmailTx(tx *gorm.DB, ssoEmail *string, currentUserID []byte) error {
|
|
if ssoEmail == nil {
|
|
return nil
|
|
}
|
|
q := tx.Table("users").Where("sso_email = ?", *ssoEmail)
|
|
if len(currentUserID) == 16 {
|
|
q = q.Where("id <> ?", currentUserID)
|
|
}
|
|
var count int64
|
|
if err := q.Limit(1).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("sso_email already exists")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func usernameExistsTx(tx *gorm.DB, username string, currentUserID []byte) (bool, error) {
|
|
username = strings.TrimSpace(username)
|
|
if username == "" {
|
|
return false, nil
|
|
}
|
|
|
|
q := tx.Table("users").Select("id").Where("username = ?", username)
|
|
if len(currentUserID) == 16 {
|
|
q = q.Where("id <> ?", currentUserID)
|
|
}
|
|
|
|
var row struct {
|
|
ID []byte `gorm:"column:id"`
|
|
}
|
|
err := q.Limit(1).Take(&row).Error
|
|
switch {
|
|
case err == nil:
|
|
return true, nil
|
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
|
return false, nil
|
|
default:
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
func (r *ContactRepository) syncAdminRoleTx(ctx context.Context, tx *gorm.DB, userID []byte, wantAdmin bool) error {
|
|
if len(userID) != 16 {
|
|
return errors.New("invalid user id")
|
|
}
|
|
primaryRoleID, err := r.primaryRoleIDForUserTx(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
roleIDs, err := listUserRoleIDsTx(tx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
adminRoleID, _, err := r.resolveRoleID(tx, "admin")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
adminKey := string(adminRoleID)
|
|
|
|
adjusted := make([][]byte, 0, len(roleIDs)+1)
|
|
for i := range roleIDs {
|
|
if len(roleIDs[i]) != 16 {
|
|
continue
|
|
}
|
|
if string(roleIDs[i]) == adminKey {
|
|
continue
|
|
}
|
|
adjusted = append(adjusted, roleIDs[i])
|
|
}
|
|
if wantAdmin {
|
|
adjusted = append(adjusted, adminRoleID)
|
|
}
|
|
return replaceUserRolesTx(ctx, tx, userID, primaryRoleID, adjusted)
|
|
}
|
|
|
|
func (r *ContactRepository) primaryRoleIDForUserTx(tx *gorm.DB, userID []byte) ([]byte, error) {
|
|
var row struct {
|
|
RoleID []byte `gorm:"column:role_id"`
|
|
}
|
|
if err := tx.Table("users").Select("role_id").Where("id = ?", userID).Take(&row).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if len(row.RoleID) != 16 {
|
|
return nil, nil
|
|
}
|
|
return row.RoleID, nil
|
|
}
|
|
|
|
func hasAnyProfilePatch(p contact.ProfilePatch) bool {
|
|
return p.PilotCategory != nil ||
|
|
p.ShortName != nil ||
|
|
p.LicenseNo != nil ||
|
|
p.LicenseIssuedAt != nil ||
|
|
p.LicenseExpiredAt != nil ||
|
|
p.TechnicianLicenseNo != nil ||
|
|
p.HasIFRQualification != nil ||
|
|
p.IsChiefPilot != nil ||
|
|
p.IsDUL != nil ||
|
|
p.DULBaseIDsSet ||
|
|
p.ResponsibleFlightRescuer != nil ||
|
|
p.TotalFlightMinutes != nil ||
|
|
p.ResponsiblePilotMinutes != nil ||
|
|
p.SecondPilotMinutes != nil ||
|
|
p.DoubleCommandMinutes != nil ||
|
|
p.FlightInstructorMinutes != nil ||
|
|
p.NightFlightMinutes != nil ||
|
|
p.IFRFlightMinutes != nil ||
|
|
p.Specialization != nil ||
|
|
p.SubSpecialization != nil ||
|
|
p.IsChiefPhysician != nil ||
|
|
p.IsChiefTechnician != nil ||
|
|
p.IsResponsibleComplaints != nil ||
|
|
p.RoleLabel != nil ||
|
|
p.Note != nil ||
|
|
p.Location != nil ||
|
|
p.Postcode != nil ||
|
|
p.StreetLine != nil ||
|
|
p.WeightKG != nil
|
|
}
|
|
|
|
func (r *ContactRepository) syncContactDULBasesTx(ctx context.Context, tx *gorm.DB, userID []byte, baseIDs [][]byte) error {
|
|
if len(userID) != 16 {
|
|
return errors.New("invalid user id")
|
|
}
|
|
|
|
if err := tx.Where("contact_id = ? AND LOWER(role_code) = ?", userID, contactBaseRoleDUL).Delete(&base.BaseContactRole{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(baseIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
validIDs := dedupeBinaryIDs(baseIDs)
|
|
if err := ensureDULBasesTx(tx, validIDs); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := make([]base.BaseContactRole, 0, len(validIDs))
|
|
actor := actorUserIDFromContext(ctx)
|
|
for i := range validIDs {
|
|
rows = append(rows, base.BaseContactRole{
|
|
BaseID: validIDs[i],
|
|
ContactID: userID,
|
|
RoleCode: contactBaseRoleDUL,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
})
|
|
}
|
|
return tx.Create(&rows).Error
|
|
}
|
|
|
|
func dedupeBinaryIDs(ids [][]byte) [][]byte {
|
|
out := make([][]byte, 0, len(ids))
|
|
seen := map[string]struct{}{}
|
|
for i := range ids {
|
|
if len(ids[i]) != 16 {
|
|
continue
|
|
}
|
|
key := string(ids[i])
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
copyID := make([]byte, 16)
|
|
copy(copyID, ids[i])
|
|
out = append(out, copyID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ensureDULBasesTx(tx *gorm.DB, ids [][]byte) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
var count int64
|
|
if err := tx.Table("bases").Where("id IN ? AND deleted_at IS NULL AND dul = 1", ids).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if int(count) != len(ids) {
|
|
return errors.New("all dul_base_ids must reference existing base with dul=true")
|
|
}
|
|
return nil
|
|
}
|