user_attribute_service.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "regexp"
  7. "strconv"
  8. "strings"
  9. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  10. )
  11. // UserAttributeService handles attribute management
  12. type UserAttributeService struct {
  13. defRepo UserAttributeDefinitionRepository
  14. valueRepo UserAttributeValueRepository
  15. }
  16. // NewUserAttributeService creates a new service instance
  17. func NewUserAttributeService(
  18. defRepo UserAttributeDefinitionRepository,
  19. valueRepo UserAttributeValueRepository,
  20. ) *UserAttributeService {
  21. return &UserAttributeService{
  22. defRepo: defRepo,
  23. valueRepo: valueRepo,
  24. }
  25. }
  26. // CreateDefinition creates a new attribute definition
  27. func (s *UserAttributeService) CreateDefinition(ctx context.Context, input CreateAttributeDefinitionInput) (*UserAttributeDefinition, error) {
  28. // Validate type
  29. if !isValidAttributeType(input.Type) {
  30. return nil, ErrInvalidAttributeType
  31. }
  32. // Check if key exists
  33. exists, err := s.defRepo.ExistsByKey(ctx, input.Key)
  34. if err != nil {
  35. return nil, fmt.Errorf("check key exists: %w", err)
  36. }
  37. if exists {
  38. return nil, ErrAttributeKeyExists
  39. }
  40. def := &UserAttributeDefinition{
  41. Key: input.Key,
  42. Name: input.Name,
  43. Description: input.Description,
  44. Type: input.Type,
  45. Options: input.Options,
  46. Required: input.Required,
  47. Validation: input.Validation,
  48. Placeholder: input.Placeholder,
  49. Enabled: input.Enabled,
  50. }
  51. if err := validateDefinitionPattern(def); err != nil {
  52. return nil, err
  53. }
  54. if err := s.defRepo.Create(ctx, def); err != nil {
  55. return nil, fmt.Errorf("create definition: %w", err)
  56. }
  57. return def, nil
  58. }
  59. // GetDefinition retrieves a definition by ID
  60. func (s *UserAttributeService) GetDefinition(ctx context.Context, id int64) (*UserAttributeDefinition, error) {
  61. return s.defRepo.GetByID(ctx, id)
  62. }
  63. // GetDefinitionByKey retrieves a definition by its unique key
  64. func (s *UserAttributeService) GetDefinitionByKey(ctx context.Context, key string) (*UserAttributeDefinition, error) {
  65. return s.defRepo.GetByKey(ctx, key)
  66. }
  67. // ListDefinitions lists all definitions
  68. func (s *UserAttributeService) ListDefinitions(ctx context.Context, enabledOnly bool) ([]UserAttributeDefinition, error) {
  69. return s.defRepo.List(ctx, enabledOnly)
  70. }
  71. // UpdateDefinition updates an existing definition
  72. func (s *UserAttributeService) UpdateDefinition(ctx context.Context, id int64, input UpdateAttributeDefinitionInput) (*UserAttributeDefinition, error) {
  73. def, err := s.defRepo.GetByID(ctx, id)
  74. if err != nil {
  75. return nil, err
  76. }
  77. if input.Name != nil {
  78. def.Name = *input.Name
  79. }
  80. if input.Description != nil {
  81. def.Description = *input.Description
  82. }
  83. if input.Type != nil {
  84. if !isValidAttributeType(*input.Type) {
  85. return nil, ErrInvalidAttributeType
  86. }
  87. def.Type = *input.Type
  88. }
  89. if input.Options != nil {
  90. def.Options = *input.Options
  91. }
  92. if input.Required != nil {
  93. def.Required = *input.Required
  94. }
  95. if input.Validation != nil {
  96. def.Validation = *input.Validation
  97. }
  98. if input.Placeholder != nil {
  99. def.Placeholder = *input.Placeholder
  100. }
  101. if input.Enabled != nil {
  102. def.Enabled = *input.Enabled
  103. }
  104. if err := validateDefinitionPattern(def); err != nil {
  105. return nil, err
  106. }
  107. if err := s.defRepo.Update(ctx, def); err != nil {
  108. return nil, fmt.Errorf("update definition: %w", err)
  109. }
  110. return def, nil
  111. }
  112. // DeleteDefinition soft-deletes a definition and hard-deletes associated values
  113. func (s *UserAttributeService) DeleteDefinition(ctx context.Context, id int64) error {
  114. // Check if definition exists
  115. _, err := s.defRepo.GetByID(ctx, id)
  116. if err != nil {
  117. return err
  118. }
  119. // First delete all values (hard delete)
  120. if err := s.valueRepo.DeleteByAttributeID(ctx, id); err != nil {
  121. return fmt.Errorf("delete values: %w", err)
  122. }
  123. // Then soft-delete the definition
  124. if err := s.defRepo.Delete(ctx, id); err != nil {
  125. return fmt.Errorf("delete definition: %w", err)
  126. }
  127. return nil
  128. }
  129. // ReorderDefinitions updates display order for multiple definitions
  130. func (s *UserAttributeService) ReorderDefinitions(ctx context.Context, orders map[int64]int) error {
  131. return s.defRepo.UpdateDisplayOrders(ctx, orders)
  132. }
  133. // GetUserAttributes retrieves all attribute values for a user
  134. func (s *UserAttributeService) GetUserAttributes(ctx context.Context, userID int64) ([]UserAttributeValue, error) {
  135. return s.valueRepo.GetByUserID(ctx, userID)
  136. }
  137. // GetBatchUserAttributes retrieves attribute values for multiple users
  138. // Returns a map of userID -> map of attributeID -> value
  139. func (s *UserAttributeService) GetBatchUserAttributes(ctx context.Context, userIDs []int64) (map[int64]map[int64]string, error) {
  140. values, err := s.valueRepo.GetByUserIDs(ctx, userIDs)
  141. if err != nil {
  142. return nil, err
  143. }
  144. result := make(map[int64]map[int64]string)
  145. for _, v := range values {
  146. if result[v.UserID] == nil {
  147. result[v.UserID] = make(map[int64]string)
  148. }
  149. result[v.UserID][v.AttributeID] = v.Value
  150. }
  151. return result, nil
  152. }
  153. // UpdateUserAttributes batch updates attribute values for a user
  154. func (s *UserAttributeService) UpdateUserAttributes(ctx context.Context, userID int64, inputs []UpdateUserAttributeInput) error {
  155. // Validate all values before updating
  156. defs, err := s.defRepo.List(ctx, true)
  157. if err != nil {
  158. return fmt.Errorf("list definitions: %w", err)
  159. }
  160. defMap := make(map[int64]*UserAttributeDefinition, len(defs))
  161. for i := range defs {
  162. defMap[defs[i].ID] = &defs[i]
  163. }
  164. for _, input := range inputs {
  165. def, ok := defMap[input.AttributeID]
  166. if !ok {
  167. return ErrAttributeDefinitionNotFound
  168. }
  169. if err := s.validateValue(def, input.Value); err != nil {
  170. return err
  171. }
  172. }
  173. return s.valueRepo.UpsertBatch(ctx, userID, inputs)
  174. }
  175. // validateValue validates a value against its definition
  176. func (s *UserAttributeService) validateValue(def *UserAttributeDefinition, value string) error {
  177. // Skip validation for empty non-required fields
  178. if value == "" && !def.Required {
  179. return nil
  180. }
  181. // Required check
  182. if def.Required && value == "" {
  183. return validationError(fmt.Sprintf("%s is required", def.Name))
  184. }
  185. v := def.Validation
  186. // String length validation
  187. if v.MinLength != nil && len(value) < *v.MinLength {
  188. return validationError(fmt.Sprintf("%s must be at least %d characters", def.Name, *v.MinLength))
  189. }
  190. if v.MaxLength != nil && len(value) > *v.MaxLength {
  191. return validationError(fmt.Sprintf("%s must be at most %d characters", def.Name, *v.MaxLength))
  192. }
  193. // Number validation
  194. if def.Type == AttributeTypeNumber && value != "" {
  195. num, err := strconv.Atoi(value)
  196. if err != nil {
  197. return validationError(fmt.Sprintf("%s must be a number", def.Name))
  198. }
  199. if v.Min != nil && num < *v.Min {
  200. return validationError(fmt.Sprintf("%s must be at least %d", def.Name, *v.Min))
  201. }
  202. if v.Max != nil && num > *v.Max {
  203. return validationError(fmt.Sprintf("%s must be at most %d", def.Name, *v.Max))
  204. }
  205. }
  206. // Pattern validation
  207. if v.Pattern != nil && *v.Pattern != "" && value != "" {
  208. re, err := regexp.Compile(*v.Pattern)
  209. if err != nil {
  210. return validationError(def.Name + " has an invalid pattern")
  211. }
  212. if !re.MatchString(value) {
  213. msg := def.Name + " format is invalid"
  214. if v.Message != nil && *v.Message != "" {
  215. msg = *v.Message
  216. }
  217. return validationError(msg)
  218. }
  219. }
  220. // Select validation
  221. if def.Type == AttributeTypeSelect && value != "" {
  222. found := false
  223. for _, opt := range def.Options {
  224. if opt.Value == value {
  225. found = true
  226. break
  227. }
  228. }
  229. if !found {
  230. return validationError(fmt.Sprintf("%s: invalid option", def.Name))
  231. }
  232. }
  233. // Multi-select validation (stored as JSON array)
  234. if def.Type == AttributeTypeMultiSelect && value != "" {
  235. var values []string
  236. if err := json.Unmarshal([]byte(value), &values); err != nil {
  237. // Try comma-separated fallback
  238. values = strings.Split(value, ",")
  239. }
  240. for _, val := range values {
  241. val = strings.TrimSpace(val)
  242. found := false
  243. for _, opt := range def.Options {
  244. if opt.Value == val {
  245. found = true
  246. break
  247. }
  248. }
  249. if !found {
  250. return validationError(fmt.Sprintf("%s: invalid option %s", def.Name, val))
  251. }
  252. }
  253. }
  254. return nil
  255. }
  256. // validationError creates a validation error with a custom message
  257. func validationError(msg string) error {
  258. return infraerrors.BadRequest("ATTRIBUTE_VALIDATION_FAILED", msg)
  259. }
  260. func isValidAttributeType(t UserAttributeType) bool {
  261. switch t {
  262. case AttributeTypeText, AttributeTypeTextarea, AttributeTypeNumber,
  263. AttributeTypeEmail, AttributeTypeURL, AttributeTypeDate,
  264. AttributeTypeSelect, AttributeTypeMultiSelect:
  265. return true
  266. }
  267. return false
  268. }
  269. func validateDefinitionPattern(def *UserAttributeDefinition) error {
  270. if def == nil {
  271. return nil
  272. }
  273. if def.Validation.Pattern == nil {
  274. return nil
  275. }
  276. pattern := strings.TrimSpace(*def.Validation.Pattern)
  277. if pattern == "" {
  278. return nil
  279. }
  280. if _, err := regexp.Compile(pattern); err != nil {
  281. return infraerrors.BadRequest("INVALID_ATTRIBUTE_PATTERN", fmt.Sprintf("invalid pattern for %s: %v", def.Name, err))
  282. }
  283. return nil
  284. }