package service import ( "baron-sso-backend/internal/domain" "crypto/rand" "errors" "fmt" "math/big" "net/mail" "os" "sort" "strconv" "strings" ) const ( WorksmobileUserActionUpsert = "UPSERT" WorksmobileUserActionSuspend = "SUSPEND" ) type WorksmobileOrgUnitPayload struct { DomainID int64 `json:"domainId"` OrgUnitName string `json:"orgUnitName"` Email string `json:"email,omitempty"` OrgUnitExternalKey string `json:"orgUnitExternalKey"` ParentOrgUnitID string `json:"parentOrgUnitId,omitempty"` DisplayOrder int `json:"displayOrder"` } type WorksmobileUserPayload struct { DomainID int64 `json:"domainId"` Email string `json:"email"` UserExternalKey string `json:"userExternalKey"` UserName WorksmobileUserName `json:"userName"` CellPhone string `json:"cellPhone,omitempty"` EmployeeNumber string `json:"employeeNumber,omitempty"` PrivateEmail string `json:"privateEmail,omitempty"` AliasEmails []string `json:"aliasEmails,omitempty"` Locale string `json:"locale,omitempty"` PasswordConfig WorksmobilePasswordConfig `json:"passwordConfig"` Task string `json:"task,omitempty"` Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"` } type WorksmobileUserName struct { LastName string `json:"lastName,omitempty"` } type WorksmobilePasswordConfig struct { PasswordCreationType string `json:"passwordCreationType"` Password string `json:"password"` } type WorksmobilePasswordResetPayload struct { Email string `json:"email"` PasswordConfig WorksmobilePasswordConfig `json:"passwordConfig"` } type WorksmobileUserOrganization struct { DomainID int64 `json:"domainId,omitempty"` Email string `json:"email,omitempty"` Primary bool `json:"primary"` OrgUnits []WorksmobileUserOrgUnit `json:"orgUnits"` } type WorksmobileUserOrgUnit struct { OrgUnitID string `json:"orgUnitId"` Primary bool `json:"primary"` PositionID string `json:"positionId,omitempty"` IsManager *bool `json:"isManager,omitempty"` } func BuildWorksmobileOrgUnitPayload(tenant domain.Tenant, rootConfig domain.JSONMap, displayOrder int) (WorksmobileOrgUnitPayload, error) { return BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant, tenant, rootConfig, displayOrder) } func BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant domain.Tenant, domainTenant domain.Tenant, rootConfig domain.JSONMap, displayOrder int) (WorksmobileOrgUnitPayload, error) { if err := ValidateWorksmobileExternalKey(tenant.ID); err != nil { return WorksmobileOrgUnitPayload{}, err } if displayOrder < 1 { displayOrder = 1 } domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig) if err != nil { return WorksmobileOrgUnitPayload{}, err } payload := WorksmobileOrgUnitPayload{ DomainID: domainID, OrgUnitName: strings.TrimSpace(tenant.Name), Email: buildWorksmobileOrgUnitEmail(tenant, domainTenant), OrgUnitExternalKey: tenant.ID, DisplayOrder: displayOrder, } if tenant.ParentID != nil && *tenant.ParentID != "" { if err := ValidateWorksmobileExternalKey(*tenant.ParentID); err != nil { return WorksmobileOrgUnitPayload{}, err } payload.ParentOrgUnitID = "externalKey:" + *tenant.ParentID } return payload, nil } func buildWorksmobileOrgUnitEmail(tenant domain.Tenant, domainTenant domain.Tenant) string { slug := strings.ToLower(strings.TrimSpace(tenant.Slug)) if slug == "" { return "" } if domainName := worksmobileTenantMailDomain(domainTenant); domainName != "" { return slug + "@" + domainName } for _, candidate := range append([]domain.TenantDomain{}, domainTenant.Domains...) { domainName := strings.ToLower(strings.TrimSpace(candidate.Domain)) if domainName != "" { return slug + "@" + domainName } } for _, candidate := range tenant.Domains { domainName := strings.ToLower(strings.TrimSpace(candidate.Domain)) if domainName != "" { return slug + "@" + domainName } } return "" } func worksmobileTenantMailDomain(tenant domain.Tenant) string { envKey := strings.TrimSuffix(worksmobileTenantDomainIDEnvKey(tenant), "_DOMAIN_ID") if domainName := strings.ToLower(strings.TrimSpace(os.Getenv("WORKS_DEFAULT_DOMAIN_" + envKey))); domainName != "" { return domainName } if domainName := strings.ToLower(strings.TrimSpace(os.Getenv(envKey + "_MAIL_DOMAIN"))); domainName != "" { return domainName } switch envKey { case "SAMAN": return "samaneng.com" case "HANMAC": return "hanmaceng.co.kr" case "GPDTDC": return "baroncs.co.kr" case "HALLA": return "hallasanup.com" case "BARONGROUP": return "brsw.kr" default: return "" } } func BuildWorksmobileUserPayload(user domain.User, tenant domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) { return BuildWorksmobileUserPayloadForDomainTenant(user, tenant, tenant, rootConfig) } func BuildWorksmobileUserPayloadForDomainTenant(user domain.User, tenant domain.Tenant, _ domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) { return BuildWorksmobileUserPayloadForDomainTenants(user, tenant, map[string]domain.Tenant{tenant.ID: tenant}, rootConfig) } func BuildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) { if err := ValidateWorksmobileExternalKey(user.ID); err != nil { return WorksmobileUserPayload{}, err } if tenant.ID == "" { return WorksmobileUserPayload{}, errors.New("tenant is required") } if tenantByID == nil { tenantByID = map[string]domain.Tenant{} } tenantByID[tenant.ID] = tenant domainID, err := ResolveWorksmobileAccountDomainIDFromEmail(user.Email, tenant, rootConfig) if err != nil { return WorksmobileUserPayload{}, err } employeeNumber := metadataEmployeeNumber(user.Metadata) organizations, task, err := buildWorksmobileUserOrganizations(user, tenant, tenantByID, rootConfig) if err != nil { return WorksmobileUserPayload{}, err } if task == "" { task = strings.TrimSpace(user.JobTitle) } payload := WorksmobileUserPayload{ DomainID: domainID, Email: strings.TrimSpace(user.Email), UserExternalKey: user.ID, UserName: WorksmobileUserName{LastName: strings.TrimSpace(user.Name)}, CellPhone: strings.TrimSpace(user.Phone), EmployeeNumber: employeeNumber, Locale: "ko_KR", PasswordConfig: WorksmobilePasswordConfig{ PasswordCreationType: "ADMIN", Password: GenerateWorksmobileInitialPassword(), }, Task: task, Organizations: organizations, } payload.AliasEmails = BuildWorksmobileAliasEmails(user, tenant) return payload, nil } type worksmobileAppointment struct { TenantID string IsPrimary bool IsManager bool HasManager bool JobTitle string PositionID string } func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) ([]WorksmobileUserOrganization, string, error) { appointments := worksmobileAppointmentsFromMetadata(user.Metadata) if len(appointments) == 0 { appointments = []worksmobileAppointment{{TenantID: tenant.ID, IsPrimary: true}} } accountDomainTenant := worksmobileAccountDomainTenantFromEmail(user.Email, tenant, tenantByID) accountDomainEnvKey := worksmobileTenantDomainIDEnvKey(accountDomainTenant) if !worksmobileAppointmentsContainDomain(appointments, tenantByID, accountDomainEnvKey) && accountDomainTenant.ID != "" { appointments = append([]worksmobileAppointment{{ TenantID: accountDomainTenant.ID, IsPrimary: true, JobTitle: strings.TrimSpace(user.JobTitle), PositionID: metadataString(user.Metadata, "worksmobilePositionId", "positionId", "position_id"), }}, appointments...) } organizations := make([]WorksmobileUserOrganization, 0) organizationIndexByDomainID := map[int64]int{} seen := map[string]bool{} task := "" for _, appointment := range appointments { if appointment.TenantID == "" || seen[appointment.TenantID] { continue } appointmentTenant, ok := tenantByID[appointment.TenantID] if !ok { continue } if err := ValidateWorksmobileExternalKey(appointmentTenant.ID); err != nil { return nil, "", err } domainTenant := worksmobileDomainClassificationTenant(appointmentTenant, tenantByID) domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig) if err != nil { return nil, "", err } isAccountDomain := worksmobileTenantDomainIDEnvKey(domainTenant) == accountDomainEnvKey isPrimaryOrganization := isAccountDomain && !worksmobileOrganizationsHavePrimary(organizations) organizationIndex, organizationExists := organizationIndexByDomainID[domainID] orgUnit := WorksmobileUserOrgUnit{ OrgUnitID: "externalKey:" + appointmentTenant.ID, Primary: !organizationExists, PositionID: appointment.PositionID, } if appointment.HasManager { isManager := appointment.IsManager orgUnit.IsManager = &isManager } if organizationExists { if isPrimaryOrganization { organizations[organizationIndex].Primary = true organizations[organizationIndex].Email = worksmobileOrganizationEmail(user, domainTenant) } organizations[organizationIndex].OrgUnits = append(organizations[organizationIndex].OrgUnits, orgUnit) } else { organizationIndexByDomainID[domainID] = len(organizations) organizations = append(organizations, WorksmobileUserOrganization{ DomainID: domainID, Email: worksmobileOrganizationEmail(user, domainTenant), Primary: isPrimaryOrganization, OrgUnits: []WorksmobileUserOrgUnit{orgUnit}, }) } if isPrimaryOrganization && strings.TrimSpace(appointment.JobTitle) != "" { task = strings.TrimSpace(appointment.JobTitle) } seen[appointment.TenantID] = true } if len(organizations) == 0 { return nil, "", errors.New("no valid worksmobile organization") } if !worksmobileOrganizationsHavePrimary(organizations) { organizations[0].Primary = true if len(organizations[0].OrgUnits) > 0 { organizations[0].OrgUnits[0].Primary = true } } sortWorksmobileOrganizations(organizations) return organizations, task, nil } func worksmobileAppointmentsContainDomain(appointments []worksmobileAppointment, tenantByID map[string]domain.Tenant, envKey string) bool { for _, appointment := range appointments { tenant, ok := tenantByID[appointment.TenantID] if !ok { continue } domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID) if worksmobileTenantDomainIDEnvKey(domainTenant) == envKey { return true } } return false } func worksmobileOrganizationsHavePrimary(organizations []WorksmobileUserOrganization) bool { for _, organization := range organizations { if organization.Primary { return true } } return false } func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileAppointment { rawAppointments, ok := metadata["additionalAppointments"].([]any) if !ok { return nil } appointments := make([]worksmobileAppointment, 0, len(rawAppointments)) for _, raw := range rawAppointments { item, ok := raw.(map[string]any) if !ok { continue } appointment := worksmobileAppointment{ TenantID: metadataString(domain.JSONMap(item), "tenantId", "tenant_id"), IsPrimary: metadataBool(domain.JSONMap(item), "isPrimary", "primary"), JobTitle: metadataString(domain.JSONMap(item), "jobTitle", "job_title", "task"), PositionID: metadataString(domain.JSONMap(item), "worksmobilePositionId", "positionId", "position_id"), } if isManager, ok := metadataOptionalBool(domain.JSONMap(item), "isManager", "lead", "isLead"); ok { appointment.IsManager = isManager appointment.HasManager = true } appointments = append(appointments, appointment) } return appointments } func sortWorksmobileOrganizations(organizations []WorksmobileUserOrganization) { sort.SliceStable(organizations, func(i, j int) bool { if organizations[i].Primary != organizations[j].Primary { return organizations[i].Primary } left := "" right := "" if len(organizations[i].OrgUnits) > 0 { left = organizations[i].OrgUnits[0].OrgUnitID } if len(organizations[j].OrgUnits) > 0 { right = organizations[j].OrgUnits[0].OrgUnitID } return left < right }) } func BuildWorksmobileAliasEmails(user domain.User, tenant domain.Tenant) []string { candidates := make([]string, 0) for _, key := range []string{ "aliasEmails", "alias_emails", "worksmobileAliasEmails", "sub_email", "secondary_email", "secondary_emails", "additional_email", "additional_emails", "naverworks_sub_email", } { candidates = append(candidates, metadataStringList(user.Metadata, key)...) } employeeNumber := metadataEmployeeNumber(user.Metadata) if isHanmacWorksmobileTenant(tenant) && employeeNumber != "" { candidates = append(candidates, employeeNumber+"@hanmaceng.co.kr") } return normalizeWorksmobileAliasEmails(user.Email, candidates) } func normalizeWorksmobileAliasEmails(primaryEmail string, candidates []string) []string { result := make([]string, 0, len(candidates)) seen := map[string]bool{} primary := strings.ToLower(strings.TrimSpace(primaryEmail)) for _, candidate := range candidates { normalized := strings.ToLower(strings.TrimSpace(candidate)) if normalized == "" || normalized == primary || seen[normalized] { continue } if _, err := mail.ParseAddress(normalized); err != nil { continue } seen[normalized] = true result = append(result, normalized) } return result } func ValidateWorksmobileAliasEmails(primaryEmail string, aliasEmails []string, existingEmails map[string]string) error { seen := map[string]string{strings.ToLower(strings.TrimSpace(primaryEmail)): primaryEmail} for _, aliasEmail := range aliasEmails { normalized := strings.ToLower(strings.TrimSpace(aliasEmail)) if _, err := mail.ParseAddress(normalized); err != nil { return err } if previous, ok := seen[normalized]; ok { return fmt.Errorf("worksmobile alias email duplicates: %s and %s", previous, aliasEmail) } if owner, ok := existingEmails[normalized]; ok { return fmt.Errorf("worksmobile alias email %s는 이미 사용 중입니다: %s", normalized, owner) } seen[normalized] = aliasEmail } return nil } func GenerateWorksmobileInitialPassword() string { digits := "0123456789" letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" symbols := "!@#$%^&*()-_=+[]{}" all := digits + letters + symbols password := []byte{ randomChar(digits), randomChar(letters), randomChar(symbols), } for len(password) < 16 { password = append(password, randomChar(all)) } shuffleBytes(password) return string(password) } func randomChar(chars string) byte { if chars == "" { return 'x' } index, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) if err != nil { return chars[0] } return chars[index.Int64()] } func shuffleBytes(values []byte) { for i := len(values) - 1; i > 0; i-- { j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) if err != nil { continue } values[i], values[j.Int64()] = values[j.Int64()], values[i] } } func WorksmobileUserStatusAction(status string) string { normalized := domain.NormalizeUserStatus(status) if domain.IsWorksDeprovisionUserStatus(normalized) { return domain.WorksmobileActionDelete } switch normalized { case domain.UserStatusSuspended: return WorksmobileUserActionSuspend default: return WorksmobileUserActionUpsert } } func ValidateWorksmobileExternalKey(value string) error { value = strings.TrimSpace(value) if value == "" { return errors.New("external key is required") } if strings.ContainsAny(value, `%\#/?`) { return fmt.Errorf("external key contains unsupported character: %s", value) } return nil } func ResolveWorksmobileDomainIDFromTenant(tenant domain.Tenant, _ domain.JSONMap) (int64, error) { envKey := worksmobileTenantDomainIDEnvKey(tenant) if domainID, ok := worksmobileDomainIDFromEnv(envKey); ok { return domainID, nil } return 0, fmt.Errorf("worksmobile domain id env is missing for tenant: %s", envKey) } func ResolveWorksmobileAccountDomainIDFromEmail(email string, fallbackTenant domain.Tenant, rootConfig domain.JSONMap) (int64, error) { switch worksmobileEmailDomainName(email) { case "samaneng.com": if domainID, ok := worksmobileDomainIDFromEnv("SAMAN_DOMAIN_ID"); ok { return domainID, nil } case "hanmaceng.co.kr": if domainID, ok := worksmobileDomainIDFromEnv("HANMAC_DOMAIN_ID"); ok { return domainID, nil } case "baroncs.co.kr": if domainID, ok := worksmobileDomainIDFromEnv("GPDTDC_DOMAIN_ID"); ok { return domainID, nil } case "hallasanup.com": if domainID, ok := worksmobileDomainIDFromEnv("HALLA_DOMAIN_ID"); ok { return domainID, nil } case "brsw.kr": if domainID, ok := worksmobileDomainIDFromEnv("BARONGROUP_DOMAIN_ID"); ok { return domainID, nil } } return ResolveWorksmobileDomainIDFromTenant(fallbackTenant, rootConfig) } func worksmobileAccountDomainTenantFromEmail(email string, fallbackTenant domain.Tenant, tenantByID map[string]domain.Tenant) domain.Tenant { envKey := worksmobileDomainIDEnvKeyFromEmail(email) for _, tenant := range tenantByID { if isWorksmobileDomainRootTenant(tenant) && worksmobileTenantDomainIDEnvKey(tenant) == envKey { return tenant } } for _, tenant := range tenantByID { if worksmobileTenantDomainIDEnvKey(tenant) == envKey { return worksmobileDomainClassificationTenant(tenant, tenantByID) } } return worksmobileDomainClassificationTenant(fallbackTenant, tenantByID) } func worksmobileDomainIDEnvKeyFromEmail(email string) string { switch worksmobileEmailDomainName(email) { case "samaneng.com": return "SAMAN_DOMAIN_ID" case "hanmaceng.co.kr": return "HANMAC_DOMAIN_ID" case "baroncs.co.kr": return "GPDTDC_DOMAIN_ID" case "hallasanup.com": return "HALLA_DOMAIN_ID" case "brsw.kr": return "BARONGROUP_DOMAIN_ID" default: return worksmobileTenantDomainIDEnvKey(domain.Tenant{}) } } func worksmobileEmailDomainName(email string) string { address, err := mail.ParseAddress(strings.TrimSpace(email)) if err != nil { return "" } parts := strings.Split(address.Address, "@") if len(parts) != 2 { return "" } return strings.ToLower(strings.TrimSpace(parts[1])) } func worksmobileOrganizationEmail(user domain.User, domainTenant domain.Tenant) string { domainName := worksmobileTenantMailDomain(domainTenant) if domainName == "" { return "" } primaryEmail := strings.ToLower(strings.TrimSpace(user.Email)) if worksmobileEmailDomainName(primaryEmail) == domainName { return primaryEmail } for _, alias := range BuildWorksmobileAliasEmails(user, domainTenant) { if worksmobileEmailDomainName(alias) == domainName { return alias } } localPart, err := domain.ExtractNormalizedEmailLocalPart(primaryEmail) if err != nil || localPart == "" { return "" } return localPart + "@" + domainName } func worksmobileTenantDomainIDEnvKey(tenant domain.Tenant) string { if tenantHasDomain(tenant, "samaneng.com") || tenantMatchesAny(tenant, "saman", "삼안") { return "SAMAN_DOMAIN_ID" } if isHanmacWorksmobileTenant(tenant) { return "HANMAC_DOMAIN_ID" } if tenantMatchesAny(tenant, "gpdtdc", "총괄", "기술개발센터", "기술개발") { return "GPDTDC_DOMAIN_ID" } if isHallaWorksmobileTenant(tenant) { return "HALLA_DOMAIN_ID" } return "BARONGROUP_DOMAIN_ID" } func worksmobileDomainIDFromEnv(key string) (int64, bool) { if key == "" { return 0, false } id, ok := parseDomainID(os.Getenv(key)) return id, ok } type worksmobileDomainEnvMapping struct { Key string Label string } func worksmobileDomainEnvMappings() []worksmobileDomainEnvMapping { return []worksmobileDomainEnvMapping{ {Key: "SAMAN_DOMAIN_ID", Label: "삼안"}, {Key: "HANMAC_DOMAIN_ID", Label: "한맥기술"}, {Key: "GPDTDC_DOMAIN_ID", Label: "총괄기획&기술개발센터"}, {Key: "HALLA_DOMAIN_ID", Label: "한라산업개발"}, {Key: "BARONGROUP_DOMAIN_ID", Label: "바론그룹"}, } } func WorksmobileDomainIDsFromEnv() []int64 { mappings := worksmobileDomainEnvMappings() result := make([]int64, 0, len(mappings)) seen := map[int64]bool{} for _, mapping := range mappings { if id, ok := worksmobileDomainIDFromEnv(mapping.Key); ok && !seen[id] { seen[id] = true result = append(result, id) } } return result } func WorksmobileDomainLabelForID(domainID int64) string { for _, mapping := range worksmobileDomainEnvMappings() { if id, ok := worksmobileDomainIDFromEnv(mapping.Key); ok && id == domainID { return mapping.Label } } return "" } func isHanmacWorksmobileTenant(tenant domain.Tenant) bool { return tenantHasDomain(tenant, "hanmaceng.co.kr") || tenantMatchesAny(tenant, "hanmac", "한맥") } func isHallaWorksmobileTenant(tenant domain.Tenant) bool { return tenantHasDomain(tenant, "hallasanup.com") || tenantMatchesAny(tenant, "halla", "hanlla", "한라산업개발") } func tenantHasDomain(tenant domain.Tenant, domainName string) bool { domainName = strings.ToLower(strings.TrimSpace(domainName)) for _, d := range tenant.Domains { if strings.EqualFold(strings.TrimSpace(d.Domain), domainName) { return true } } return false } func tenantMatchesAny(tenant domain.Tenant, needles ...string) bool { haystack := strings.ToLower(strings.TrimSpace(tenant.Slug + " " + tenant.Name)) for _, needle := range needles { if strings.Contains(haystack, strings.ToLower(strings.TrimSpace(needle))) { return true } } return false } func WorksmobileEnabled(rootConfig domain.JSONMap) bool { rawWorksmobile, ok := rootConfig["worksmobile"].(map[string]any) if !ok { if raw, ok := rootConfig["worksmobile"].(domain.JSONMap); ok { rawWorksmobile = map[string]any(raw) } else { return false } } enabled, _ := rawWorksmobile["enabled"].(bool) return enabled } func WorksmobileDomainMappings(rootConfig domain.JSONMap) map[string]int64 { result := map[string]int64{} rawWorksmobile, ok := rootConfig["worksmobile"].(map[string]any) if !ok { if raw, ok := rootConfig["worksmobile"].(domain.JSONMap); ok { rawWorksmobile = map[string]any(raw) } else { return result } } rawMappings, ok := rawWorksmobile["domainMappings"].(map[string]any) if !ok { if raw, ok := rawWorksmobile["domainMappings"].(domain.JSONMap); ok { rawMappings = map[string]any(raw) } else { return result } } for key, raw := range rawMappings { if id, ok := parseDomainID(raw); ok { result[strings.ToLower(strings.TrimSpace(key))] = id } } return result } func parseDomainID(raw any) (int64, bool) { switch value := raw.(type) { case int: return int64(value), value > 0 case int64: return value, value > 0 case float64: id := int64(value) return id, id > 0 case string: id, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) return id, err == nil && id > 0 default: return 0, false } } func metadataString(metadata domain.JSONMap, keys ...string) string { for _, key := range keys { if value, ok := metadata[key]; ok { switch v := value.(type) { case string: return strings.TrimSpace(v) default: return strings.TrimSpace(fmt.Sprint(v)) } } } return "" } func metadataEmployeeNumber(metadata domain.JSONMap) string { for _, key := range []string{"employee_id", "employeeNumber", "employee_number"} { value, ok := metadata[key] if !ok { continue } if normalized := normalizeMetadataEmployeeNumber(value); normalized != "" { return normalized } } return "" } func normalizeMetadataEmployeeNumber(value any) string { switch v := value.(type) { case string: return strings.TrimSpace(v) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: return strings.TrimSpace(fmt.Sprint(v)) case map[string]any: return normalizeMetadataCharacterMap(v) case domain.JSONMap: return normalizeMetadataCharacterMap(map[string]any(v)) case map[string]string: converted := make(map[string]any, len(v)) for key, value := range v { converted[key] = value } return normalizeMetadataCharacterMap(converted) default: return "" } } func normalizeMetadataCharacterMap(value map[string]any) string { type characterEntry struct { index int value string } entries := make([]characterEntry, 0, len(value)) for key, raw := range value { index, err := strconv.Atoi(key) if err != nil { return "" } part, ok := raw.(string) if !ok || part == "" { return "" } entries = append(entries, characterEntry{index: index, value: part}) } if len(entries) == 0 { return "" } sort.Slice(entries, func(i, j int) bool { return entries[i].index < entries[j].index }) var builder strings.Builder for _, entry := range entries { builder.WriteString(entry.value) } return strings.TrimSpace(builder.String()) } func metadataBool(metadata domain.JSONMap, keys ...string) bool { value, _ := metadataOptionalBool(metadata, keys...) return value } func metadataOptionalBool(metadata domain.JSONMap, keys ...string) (bool, bool) { for _, key := range keys { value, ok := metadata[key] if !ok { continue } switch v := value.(type) { case bool: return v, true case string: normalized := strings.ToLower(strings.TrimSpace(v)) if normalized == "true" || normalized == "1" || normalized == "yes" { return true, true } if normalized == "false" || normalized == "0" || normalized == "no" { return false, true } case int: return v != 0, true case float64: return v != 0, true } } return false, false } func metadataStringList(metadata domain.JSONMap, keys ...string) []string { for _, key := range keys { value, ok := metadata[key] if !ok { continue } switch v := value.(type) { case []string: return splitWorksmobileAliasValues(v) case []any: values := make([]string, 0, len(v)) for _, item := range v { values = append(values, strings.TrimSpace(fmt.Sprint(item))) } return splitWorksmobileAliasValues(values) case string: return splitWorksmobileAliasValues([]string{v}) default: return splitWorksmobileAliasValues([]string{fmt.Sprint(v)}) } } return nil } func splitWorksmobileAliasValues(values []string) []string { result := make([]string, 0, len(values)) for _, value := range values { fields := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t' }) for _, field := range fields { if trimmed := strings.TrimSpace(field); trimmed != "" { result = append(result, trimmed) } } } return result }