package handlers import ( "context" "encoding/hex" "errors" "fmt" "sort" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "gorm.io/gorm" "wucher/internal/domain/base" "wucher/internal/domain/contact" "wucher/internal/domain/dul" filemanager "wucher/internal/domain/file_manager" "wucher/internal/shared/pkg/apperrorsx" "wucher/internal/shared/pkg/uuidv7" "wucher/internal/transport/http/dto" "wucher/internal/transport/http/jsonapi" "wucher/internal/transport/http/response" "wucher/internal/transport/http/validators" ) type DULHandler struct { svc dul.Service baseSvc base.Service contactSvc contact.Service fileManager filemanager.Service storage fileManagerDownloadURLStorage validate *validators.Validator } func NewDULHandler(svc dul.Service) *DULHandler { return &DULHandler{svc: svc, validate: validators.New()} } func (h *DULHandler) WithFileManagerService(fileManager filemanager.Service) *DULHandler { h.fileManager = fileManager return h } func (h *DULHandler) WithBaseService(baseSvc base.Service) *DULHandler { h.baseSvc = baseSvc return h } func (h *DULHandler) WithContactService(contactSvc contact.Service) *DULHandler { h.contactSvc = contactSvc return h } func (h *DULHandler) WithFileStorage(storage fileManagerDownloadURLStorage) *DULHandler { h.storage = storage return h } // CreateDUL godoc // @Summary Create DUL // @Description Possible error code: DUL_INVALID_JSON, DUL_INVALID_UUID, DUL_DATE_INVALID, DUL_VALIDATION_FAILED, DUL_CREATE_FAILED. // @Tags DUL // @Accept json // @Produce json // @Param request body dto.DULCreateRequest true "DUL create request" // @Success 201 {object} dto.DULResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/dul/create [post] func (h *DULHandler) Create(c *fiber.Ctx) error { var req dto.DULCreateRequest if err := c.BodyParser(&req); err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil) } if errs := h.validate.ValidateStruct(req); errs != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs) } baseID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.BaseID)) if err != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "base_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/data/attributes/base_id"}) } dateValue, err := parseDULDate(strings.TrimSpace(req.Data.Attributes.Date)) if err != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "date must use format YYYY-MM-DD", &jsonapi.ErrorSource{Pointer: "/data/attributes/date"}) } row := &dul.DUL{ Name: strings.TrimSpace(req.Data.Attributes.Name), Date: dateValue, Info: strings.TrimSpace(stringOrDefault(req.Data.Attributes.Info, "")), BaseID: baseID, CreatedBy: actorUserID(c), UpdatedBy: actorUserID(c), } if row.Name == "" { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "name is required", &jsonapi.ErrorSource{Pointer: "/data/attributes/name"}) } fileUUIDs := req.Data.Attributes.FileUUID if fileUUIDs == nil { fileUUIDs = req.Data.Attributes.ImageAttachmentIDs } if err := h.validateDULImageFiles(c, fileUUIDs); err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "Create failed", err.Error(), nil) } createErr := h.withDULTransaction(c.UserContext(), func(txSvc dul.Service) error { if err := txSvc.Create(c.UserContext(), row); err != nil { return err } if err := h.syncImages(c, row.ID, fileUUIDs); err != nil { return err } return nil }) if createErr != nil { return writeDULErrors(c, fiber.StatusBadRequest, "Create failed", createErr.Error(), nil) } created, _ := h.svc.GetByID(c.UserContext(), row.ID) if created == nil { created = row } return response.Write(c, fiber.StatusCreated, dto.DULResponse{Data: h.toResource(created)}) } // UpdateDUL godoc // @Summary Update DUL // @Description Possible error code: DUL_INVALID_JSON, DUL_INVALID_UUID, DUL_DATE_INVALID, DUL_VALIDATION_FAILED, DUL_NOT_FOUND, DUL_UPDATE_FAILED. // @Tags DUL // @Accept json // @Produce json // @Param id path string true "DUL ID" // @Param request body dto.DULUpdateRequest true "DUL update request" // @Success 200 {object} dto.DULResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 404 {object} dto.ErrorResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/dul/update/{id} [patch] func (h *DULHandler) Update(c *fiber.Ctx) error { id, err := uuidv7.ParseString(c.Params("id")) if err != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/path/id"}) } var req dto.DULUpdateRequest if err := c.BodyParser(&req); err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil) } if errs := h.validate.ValidateStruct(req); errs != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs) } row, err := h.svc.GetByID(c.UserContext(), id) if err != nil || row == nil { return writeDULErrors(c, fiber.StatusNotFound, "Not found", "dul not found", nil) } if req.Data.Attributes.Name != nil { row.Name = strings.TrimSpace(*req.Data.Attributes.Name) } if req.Data.Attributes.Date != nil { dateValue, parseErr := parseDULDate(strings.TrimSpace(*req.Data.Attributes.Date)) if parseErr != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "date must use format YYYY-MM-DD", &jsonapi.ErrorSource{Pointer: "/data/attributes/date"}) } row.Date = dateValue } if req.Data.Attributes.Info != nil { row.Info = strings.TrimSpace(*req.Data.Attributes.Info) } if req.Data.Attributes.BaseID != nil { baseID, parseErr := uuidv7.ParseString(strings.TrimSpace(*req.Data.Attributes.BaseID)) if parseErr != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "base_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/data/attributes/base_id"}) } row.BaseID = baseID } row.UpdatedBy = actorUserID(c) fileUUIDPatch := req.Data.Attributes.FileUUID if fileUUIDPatch == nil && req.Data.Attributes.ImageAttachmentIDs != nil { fileUUIDPatch = req.Data.Attributes.ImageAttachmentIDs } if err := h.validateDULImageFiles(c, fileUUIDPatch); err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "Update failed", err.Error(), nil) } updateErr := h.withDULTransaction(c.UserContext(), func(txSvc dul.Service) error { if err := txSvc.Update(c.UserContext(), row); err != nil { return err } if fileUUIDPatch != nil { if err := h.syncImages(c, row.ID, fileUUIDPatch); err != nil { return err } } return nil }) if updateErr != nil { return writeDULErrors(c, fiber.StatusBadRequest, "Update failed", updateErr.Error(), nil) } updated, _ := h.svc.GetByID(c.UserContext(), row.ID) if updated == nil { updated = row } return response.Write(c, fiber.StatusOK, dto.DULResponse{Data: h.toResource(updated)}) } // DeleteDUL godoc // @Summary Delete DUL // @Description Possible error code: DUL_INVALID_UUID, DUL_DELETE_FAILED. // @Tags DUL // @Produce json // @Param id path string true "DUL ID" // @Success 200 {object} dto.DULDeleteResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 404 {object} dto.ErrorResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/dul/delete/{id} [delete] func (h *DULHandler) Delete(c *fiber.Ctx) error { id, err := uuidv7.ParseString(c.Params("id")) if err != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/path/id"}) } if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return writeDULErrors(c, fiber.StatusNotFound, "Not found", "dul not found", nil) } return writeDULErrors(c, fiber.StatusBadRequest, "Delete failed", err.Error(), nil) } return response.Write(c, fiber.StatusOK, jsonapi.Resource{Type: "dul_delete", Attributes: map[string]any{"deleted": true}}) } // GetDUL godoc // @Summary Get DUL by ID // @Description Possible error code: DUL_INVALID_UUID, DUL_NOT_FOUND. // @Tags DUL // @Produce json // @Param id path string true "DUL ID" // @Success 200 {object} dto.DULResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 404 {object} dto.ErrorResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/dul/get/{id} [get] func (h *DULHandler) Get(c *fiber.Ctx) error { id, err := uuidv7.ParseString(c.Params("id")) if err != nil { return writeDULErrors(c, fiber.StatusUnprocessableEntity, "Validation error", "id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/path/id"}) } row, err := h.svc.GetByID(c.UserContext(), id) if err != nil || row == nil { return writeDULErrors(c, fiber.StatusNotFound, "Not found", "dul not found", nil) } return response.Write(c, fiber.StatusOK, dto.DULResponse{Data: h.toResource(row)}) } // ListDUL godoc // @Summary List DUL grouped by base // @Description Possible error code: DUL_LIST_FAILED. // @Tags DUL // @Produce json // @Param search query string false "Search by name/info" // @Param page[number] query int false "Page number (default 1)" // @Param page[size] query int false "Page size (default 20, max 100)" // @Param page query int false "Page number alias" // @Param size query int false "Page size alias" // @Param limit query int false "Page size alias" // @Param sort query string false "Sort order" // @Success 200 {object} dto.DULListResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/dul/get-all [get] func (h *DULHandler) List(c *fiber.Ctx) error { pageNumber := parseIntDefault(c.Query("page[number]"), 0) if pageNumber <= 0 { pageNumber = parseIntDefault(c.Query("page"), 1) } if pageNumber < 1 { pageNumber = 1 } pageSize := parseIntDefault(c.Query("page[size]"), 0) if pageSize <= 0 { pageSize = parseIntDefault(c.Query("limit"), 0) } if pageSize <= 0 { pageSize = parseIntDefault(c.Query("size"), 20) } if pageSize > 100 { pageSize = 100 } if pageSize < 1 { pageSize = 20 } offset := (pageNumber - 1) * pageSize _ = offset allowedBaseIDs, allowedBaseIDSet, err := h.resolveScopedDULBaseAccess(c) if err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "List failed", err.Error(), nil) } rows, total, err := h.svc.List(c.UserContext(), c.Query("search"), c.Query("sort"), 0, 0, allowedBaseIDs) if err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "List failed", err.Error(), nil) } grouped := h.groupRows(rows, allowedBaseIDSet) meta := map[string]any{ "page_number": pageNumber, "page_size": pageSize, "total": total, "total_bases": len(grouped), } return response.WriteWithMeta(c, fiber.StatusOK, grouped, meta) } // ListDULDatatable godoc // @Summary List DUL grouped by base (datatable) // @Description Possible error code: DUL_LIST_FAILED. // @Tags DUL // @Produce json // @Param page query int false "Page number (default 1)" // @Param size query int false "Page size (default 20, max 100)" // @Param limit query int false "Page size override (same as size)" // @Param draw query int false "Draw counter (optional)" // @Param search query string false "Search by name/info" // @Success 200 {object} dto.DULDataTableResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/dul/get-all/dt [get] func (h *DULHandler) ListDatatable(c *fiber.Ctx) error { pageNumber := parseIntDefault(c.Query("page"), 1) if pageNumber < 1 { pageNumber = 1 } length := parseIntDefault(c.Query("limit"), 0) if length <= 0 { length = parseIntDefault(c.Query("size"), 20) } if length > 100 { length = 100 } if length < 1 { length = 20 } draw := parseIntDefault(c.Query("draw"), pageNumber) start := (pageNumber - 1) * length allowedBaseIDs, allowedBaseIDSet, err := h.resolveScopedDULBaseAccess(c) if err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "List failed", err.Error(), nil) } rows, total, err := h.svc.List(c.UserContext(), c.Query("search"), c.Query("sort"), length, start, allowedBaseIDs) if err != nil { return writeDULErrors(c, fiber.StatusBadRequest, "List failed", err.Error(), nil) } grouped := h.groupRows(rows, allowedBaseIDSet) meta := map[string]any{ "draw": draw, "records_total": total, "records_filtered": total, "page_number": pageNumber, "page_size": length, "total": total, "total_bases": len(grouped), } return response.WriteWithMeta(c, fiber.StatusOK, grouped, meta) } func (h *DULHandler) groupRows(rows []dul.DUL, allowedBaseIDSet map[string]struct{}) []dto.DULGroup { basesOrder := make([]string, 0) groups := map[string]*dto.DULGroup{} // Always seed groups with bases where dul=true so FE can always render all DUL bases. if h.baseSvc != nil { bases, _, err := h.baseSvc.ListBases(context.Background(), "", "", 0, 0, "", boolPtrDUL(true)) if err == nil { for i := range bases { if !bases[i].DUL { continue } baseID, convErr := uuidv7.BytesToString(bases[i].ID) if convErr != nil || strings.TrimSpace(baseID) == "" { continue } if len(allowedBaseIDSet) > 0 { if _, ok := allowedBaseIDSet[baseID]; !ok { continue } } if _, exists := groups[baseID]; exists { continue } groups[baseID] = &dto.DULGroup{ Base: dto.DULBaseRef{ ID: baseID, Name: bases[i].BaseName, Category: bases[i].BaseCategory.Name, }, DULs: []dto.DULResource{}, } basesOrder = append(basesOrder, baseID) } } } for i := range rows { item := rows[i] baseID, _ := uuidv7.BytesToString(item.BaseID) if strings.TrimSpace(baseID) == "" { continue } if len(allowedBaseIDSet) > 0 { if _, ok := allowedBaseIDSet[baseID]; !ok { continue } } g := groups[baseID] if g == nil { g = &dto.DULGroup{Base: dto.DULBaseRef{ID: baseID}} if item.Base != nil { g.Base.Name = item.Base.BaseName g.Base.Category = item.Base.BaseCategory.Name } groups[baseID] = g basesOrder = append(basesOrder, baseID) } g.DULs = append(g.DULs, h.toResource(&item)) } out := make([]dto.DULGroup, 0, len(basesOrder)) for _, key := range basesOrder { sort.Slice(groups[key].DULs, func(i, j int) bool { return groups[key].DULs[i].Attributes.No < groups[key].DULs[j].Attributes.No }) out = append(out, *groups[key]) } return out } func (h *DULHandler) toResource(row *dul.DUL) dto.DULResource { id, _ := uuidv7.BytesToString(row.ID) baseID, _ := uuidv7.BytesToString(row.BaseID) res := dto.DULResource{Type: "dul", ID: id, Attributes: dto.DULAttributes{Name: row.Name, Date: row.Date.Format("2006-01-02"), Info: row.Info, No: row.No, Base: dto.DULBaseRef{ID: baseID}, Images: []dto.DULImage{}, CreatedAt: row.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: row.UpdatedAt.Format("2006-01-02T15:04:05Z")}} if row.Base != nil { res.Attributes.Base.Name = row.Base.BaseName res.Attributes.Base.Category = row.Base.BaseCategory.Name } res.Attributes.CreatedBy = idString(row.CreatedBy) res.Attributes.UpdatedBy = idString(row.UpdatedBy) for _, img := range row.Images { if img == nil { continue } attID, _ := uuidv7.BytesToString(img.ID) it := dto.DULImage{UUID: attID} if img.File != nil { asset := resolveFileManagerDownloadURLs(context.Background(), h.storage, img.File) it.DownloadURL = strings.TrimSpace(asset.OriginalURL) it.OriginalSizeBytes = asset.OriginalSizeBytes it.ThumbnailDownloadURL = strings.TrimSpace(asset.ThumbnailURL) } res.Attributes.Images = append(res.Attributes.Images, it) } return res } func (h *DULHandler) syncImages(c *fiber.Ctx, dulID []byte, attachmentIDs []string) error { if h.fileManager == nil { return nil } refID := idString(dulID) desired, err := h.resolveDULImageFiles(c, attachmentIDs) if err != nil { return err } existing, _, err := h.fileManager.ListAttachmentsByReference(c.UserContext(), filemanager.ListAttachmentsParams{RefType: "dul", RefID: refID, Limit: 1000}) if err != nil { return err } for i := range existing { if delErr := h.fileManager.DeleteAttachment(c.UserContext(), existing[i].ID); delErr != nil { return delErr } } for idx, fileID := range desired { if _, createErr := h.fileManager.CreateAttachment(c.UserContext(), filemanager.CreateAttachmentParams{FileID: fileID, RefType: "dul", RefID: refID, Category: ptrString("image"), SortOrder: idx, IsPrimary: idx == 0, ActorID: actorUserID(c)}); createErr != nil { return createErr } } return nil } func (h *DULHandler) validateDULImageFiles(c *fiber.Ctx, attachmentIDs []string) error { _, err := h.resolveDULImageFiles(c, attachmentIDs) return err } func (h *DULHandler) resolveDULImageFiles(c *fiber.Ctx, attachmentIDs []string) ([][]byte, error) { if h.fileManager == nil { return nil, nil } desired := make([][]byte, 0, len(attachmentIDs)) seenFile := make(map[string]struct{}, len(attachmentIDs)) for idx, raw := range attachmentIDs { id, parseErr := uuidv7.ParseString(strings.TrimSpace(raw)) if parseErr != nil { return nil, fmt.Errorf("file_uuid[%d] is invalid UUID", idx) } att, getErr := h.fileManager.GetAttachmentByID(c.UserContext(), id) if getErr != nil { return nil, fmt.Errorf("file_uuid[%d] not found", idx) } var fileID []byte if att != nil && len(att.FileID) > 0 { fileRow, fileErr := h.fileManager.GetFileByID(c.UserContext(), att.FileID) if fileErr != nil || fileRow == nil { return nil, fmt.Errorf("file_uuid[%d] not found", idx) } if filemanager.NormalizeFileStatus(fileRow.Status) != filemanager.FileStatusReady { return nil, filemanager.ErrFileNotReadyForAttachment } fileID = append([]byte(nil), att.FileID...) } else { fileRow, fileErr := h.fileManager.GetFileByID(c.UserContext(), id) if fileErr != nil || fileRow == nil { return nil, fmt.Errorf("file_uuid[%d] not found", idx) } if filemanager.NormalizeFileStatus(fileRow.Status) != filemanager.FileStatusReady { return nil, filemanager.ErrFileNotReadyForAttachment } fileID = append([]byte(nil), fileRow.ID...) } key := string(fileID) if _, dup := seenFile[key]; dup { continue } seenFile[key] = struct{}{} desired = append(desired, fileID) } return desired, nil } type dulTransactionalService interface { WithTransaction(ctx context.Context, fn func(dul.Service) error) error } func (h *DULHandler) withDULTransaction(ctx context.Context, fn func(dul.Service) error) error { txSvc, ok := h.svc.(dulTransactionalService) if !ok { return fn(h.svc) } return txSvc.WithTransaction(ctx, fn) } func writeDULErrors(c *fiber.Ctx, status int, title, detail string, source *jsonapi.ErrorSource) error { def := inferDULErrorDef(status, title, detail) return response.WriteErrors(c, status, []jsonapi.ErrorObject{{ Status: strconv.Itoa(status), Title: def.Title, Detail: def.Message, Source: source, Code: def.Code, ErrorCode: def.ErrorCode, }}) } func inferDULErrorDef(status int, title, detail string) apperrorsx.Def { lowerTitle := strings.ToLower(strings.TrimSpace(title)) lowerDetail := strings.ToLower(strings.TrimSpace(detail)) switch status { case fiber.StatusNotFound: return apperrorsx.ErrDULNotFound case fiber.StatusUnprocessableEntity: switch { case strings.Contains(lowerDetail, "date"): return apperrorsx.ErrDULDateInvalid case strings.Contains(lowerDetail, "invalid uuid"), strings.Contains(lowerDetail, "invalid id"): return apperrorsx.ErrDULInvalidUUID case strings.Contains(lowerTitle, "validation error"): return apperrorsx.ErrDULValidationFailed default: return apperrorsx.ErrDULInvalidPayload } case fiber.StatusBadRequest: switch { case strings.Contains(lowerTitle, "invalid json"): return apperrorsx.ErrDULInvalidJSON case strings.Contains(lowerTitle, "create failed"): return apperrorsx.ErrDULCreateFailed case strings.Contains(lowerTitle, "update failed"): return apperrorsx.ErrDULUpdateFailed case strings.Contains(lowerTitle, "delete failed"): return apperrorsx.ErrDULDeleteFailed case strings.Contains(lowerTitle, "list failed"): return apperrorsx.ErrDULListFailed default: return apperrorsx.ErrDULInvalidPayload } default: return apperrorsx.ErrInternal } } func inferDULErrorCode(status int, title, detail string) (string, string) { return inferDULErrorDef(status, title, detail).Code, inferDULErrorDef(status, title, detail).ErrorCode } func parseDULDate(raw string) (time.Time, error) { return time.Parse("2006-01-02", raw) } func boolPtrDUL(v bool) *bool { return &v } func (h *DULHandler) resolveScopedDULBaseAccess(c *fiber.Ctx) ([][]byte, map[string]struct{}, error) { // Default scope for all roles: all bases with dul=true. dulBaseIDs, dulBaseIDSet, err := h.dulEnabledBaseScope() if err != nil { return nil, nil, err } // No contact service means no actor-role scoping. if h.contactSvc == nil { return dulBaseIDs, dulBaseIDSet, nil } isAdmin, err := h.contactSvc.IsActorAdmin(c.UserContext()) if err != nil { return nil, nil, err } if isAdmin { return dulBaseIDs, dulBaseIDSet, nil } actorID := actorUserID(c) if len(actorID) != 16 { return dulBaseIDs, dulBaseIDSet, nil } contactRow, err := h.contactSvc.GetContactByID(c.UserContext(), actorID) if err != nil { return nil, nil, err } if contactRow == nil { return dulBaseIDs, dulBaseIDSet, nil } if !isPilotRoleContact(contactRow) { return dulBaseIDs, dulBaseIDSet, nil } ids, set := parseDULBaseIDsFromRaw(contactRow.BaseRolesRaw) if len(dulBaseIDSet) == 0 { return ids, set, nil } // Pilot scope: all DUL data, but only within assigned DUL bases. scopedIDs := make([][]byte, 0, len(ids)) scopedSet := map[string]struct{}{} for i := range ids { baseID, convErr := uuidv7.BytesToString(ids[i]) if convErr != nil || strings.TrimSpace(baseID) == "" { continue } if _, ok := dulBaseIDSet[baseID]; !ok { continue } if _, exists := scopedSet[baseID]; exists { continue } copied := make([]byte, len(ids[i])) copy(copied, ids[i]) scopedIDs = append(scopedIDs, copied) scopedSet[baseID] = struct{}{} } return scopedIDs, scopedSet, nil } func (h *DULHandler) dulEnabledBaseScope() ([][]byte, map[string]struct{}, error) { if h.baseSvc == nil { return nil, nil, nil } bases, _, err := h.baseSvc.ListBases(context.Background(), "", "", 0, 0, "", boolPtrDUL(true)) if err != nil { return nil, nil, err } baseIDs := make([][]byte, 0, len(bases)) baseIDSet := map[string]struct{}{} for i := range bases { if !bases[i].DUL || len(bases[i].ID) != 16 { continue } baseID, convErr := uuidv7.BytesToString(bases[i].ID) if convErr != nil || strings.TrimSpace(baseID) == "" { continue } if _, exists := baseIDSet[baseID]; exists { continue } copied := make([]byte, 16) copy(copied, bases[i].ID) baseIDs = append(baseIDs, copied) baseIDSet[baseID] = struct{}{} } return baseIDs, baseIDSet, nil } func isPilotRoleContact(item *contact.ContactListItem) bool { if item == nil { return false } if strings.TrimSpace(strings.ToLower(item.RoleCode)) == "pilot" { return true } if item.RolesRaw == nil || strings.TrimSpace(*item.RolesRaw) == "" { return false } parts := strings.Split(*item.RolesRaw, ",") for i := range parts { fields := strings.Split(parts[i], "|") if len(fields) != 3 { continue } if strings.TrimSpace(strings.ToLower(fields[2])) == "pilot" { return true } } return false } func parseDULBaseIDsFromRaw(raw *string) ([][]byte, map[string]struct{}) { if raw == nil || strings.TrimSpace(*raw) == "" { return [][]byte{}, map[string]struct{}{} } out := make([][]byte, 0) set := map[string]struct{}{} parts := strings.Split(*raw, ",") for i := range parts { fields := strings.Split(parts[i], "|") if len(fields) < 3 { continue } if strings.TrimSpace(strings.ToLower(fields[2])) != "dul" { continue } baseIDHex := strings.TrimSpace(fields[0]) if baseIDHex == "" { continue } baseIDBytes, err := hex.DecodeString(baseIDHex) if err != nil || len(baseIDBytes) != 16 { continue } baseID, err := uuidv7.BytesToString(baseIDBytes) if err != nil || strings.TrimSpace(baseID) == "" { continue } if _, exists := set[baseID]; exists { continue } set[baseID] = struct{}{} copied := make([]byte, 16) copy(copied, baseIDBytes) out = append(out, copied) } return out, set }