package service import ( "context" "net/http" "strings" "time" "wucher/internal/domain/base" "wucher/internal/shared/pkg/uuidv7" ) type BaseWriteError struct { Status int Title string Detail string Pointer string } func (e *BaseWriteError) Error() string { if e == nil { return "" } return e.Detail } type BaseWriteResult struct { Row *base.Base CategoryType string PreviousFotoAttachmentID []byte } type BaseOperationalShiftTimeInput struct { DateStart time.Time DateEnd time.Time StartTimeType string EndTimeType string ShiftStart *time.Time ShiftEnd *time.Time } type BaseCreateInput struct { ID []byte CategoryType string BaseName string BaseAbbreviation string Address string Latitude float64 Longitude float64 LandlineNumber string MobileNumber string Email string SortKey *int SMSAlert bool Checklist bool LegTime string DefaultStartTimeType string DefaultEndTimeType string DefaultShiftStart *time.Time DefaultShiftEnd *time.Time UTC string Dry bool ControlCenter bool DUL bool Notes string HEMSEDCContactIDs [][]byte MedPaxContactIDs [][]byte ResponsiblePilotContactIDs [][]byte OperationalShiftTimes []BaseOperationalShiftTimeInput IsActive bool CreatedBy []byte UpdatedBy []byte FotoAttachmentID []byte } type BaseUpdateInput struct { ID []byte CategoryType string BaseName *string BaseAbbreviation *string Address *string Latitude *float64 Longitude *float64 LandlineNumber *string MobileNumber *string Email *string SortKeySet bool SortKeyValid bool SortKeyValue *int SMSAlert *bool Checklist *bool LegTime *string DefaultStartTimeType *string DefaultEndTimeType *string DefaultShiftStart *time.Time DefaultShiftEnd *time.Time UTC *string Dry *bool ControlCenter *bool DUL *bool Notes *string HEMSEDCContactIDs [][]byte HEMSEDCContactIDsSet bool MedPaxContactIDs [][]byte MedPaxContactIDsSet bool ResponsiblePilotContactIDs [][]byte ResponsiblePilotContactIDsSet bool OperationalShiftTimes []BaseOperationalShiftTimeInput OperationalShiftTimesSet bool IsActive *bool CreatedBy []byte CreatedBySet bool UpdatedBy []byte UpdatedBySet bool FotoAttachmentID []byte FotoAttachmentSet bool } func (s *BaseService) CreateDetailed(ctx context.Context, in BaseCreateInput) (*BaseWriteResult, error) { if s == nil || s.repo == nil { return nil, &BaseWriteError{Status: http.StatusInternalServerError, Title: "Create failed", Detail: "base service is not configured"} } category, ok := base.NormalizeCategoryType(in.CategoryType) if !ok { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "invalid base category type", Pointer: "/data/attributes/base_category"} } baseName := strings.TrimSpace(in.BaseName) if baseName == "" { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "base is required", Pointer: "/data/attributes/base"} } if in.Latitude < -90 || in.Latitude > 90 { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "latitude must be between -90 and 90", Pointer: "/data/attributes/latitude"} } if in.Longitude < -180 || in.Longitude > 180 { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "longitude must be between -180 and 180", Pointer: "/data/attributes/longitude"} } defaultStartTimeType := normalizeShiftTimeType(in.DefaultStartTimeType) defaultEndTimeType := normalizeShiftTimeType(in.DefaultEndTimeType) if defaultStartTimeType == base.ShiftTimeTypeFixed && in.DefaultShiftStart == nil { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_start is required", Pointer: "/data/attributes/default_shift_start"} } if defaultEndTimeType == base.ShiftTimeTypeFixed && in.DefaultShiftEnd == nil { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_end is required", Pointer: "/data/attributes/default_shift_end"} } row := &base.Base{ ID: append([]byte(nil), in.ID...), BaseName: baseName, BaseAbbreviation: strings.TrimSpace(in.BaseAbbreviation), Address: strings.TrimSpace(in.Address), Latitude: in.Latitude, Longitude: in.Longitude, LandlineNumber: strings.TrimSpace(in.LandlineNumber), MobileNumber: strings.TrimSpace(in.MobileNumber), Email: strings.TrimSpace(in.Email), SortKey: cloneIntPtr(in.SortKey), SMSAlert: in.SMSAlert, Checklist: in.Checklist, LegTime: strings.TrimSpace(in.LegTime), DefaultStartTimeType: defaultStartTimeType, DefaultEndTimeType: defaultEndTimeType, DefaultShiftStart: timePtrToClockString(in.DefaultShiftStart), DefaultShiftEnd: timePtrToClockString(in.DefaultShiftEnd), UTC: normalizeUTCString(in.UTC), Dry: in.Dry, ControlCenter: in.ControlCenter, DUL: in.DUL, Notes: strings.TrimSpace(in.Notes), IsActive: in.IsActive, HEMSEDCContactIDs: appendUniqueIDs(in.HEMSEDCContactIDs), MedPaxContactIDs: appendUniqueIDs(in.MedPaxContactIDs), ResponsiblePilotContactIDs: appendUniqueIDs(in.ResponsiblePilotContactIDs), OperationalShiftTimes: cloneOperationalShiftTimeInputs(in.OperationalShiftTimes), CreatedBy: append([]byte(nil), in.CreatedBy...), UpdatedBy: append([]byte(nil), in.UpdatedBy...), FotoAttachmentID: append([]byte(nil), in.FotoAttachmentID...), } if len(row.ID) == 0 { row.ID = uuidv7.MustBytes() } if err := s.CreateBase(ctx, row, category); err != nil { return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()} } created, err := s.GetBaseByID(ctx, row.ID, category) if err != nil { return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()} } if created == nil { created = row } return &BaseWriteResult{Row: created, CategoryType: category}, nil } func (s *BaseService) UpdateDetailed(ctx context.Context, in BaseUpdateInput) (*BaseWriteResult, error) { if s == nil || s.repo == nil { return nil, &BaseWriteError{Status: http.StatusInternalServerError, Title: "Update failed", Detail: "base service is not configured"} } category, ok := base.NormalizeCategoryType(in.CategoryType) if !ok { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "invalid base category type", Pointer: "/data/attributes/base_category"} } if len(in.ID) != 16 { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "uuid is invalid UUID", Pointer: "/path/uuid"} } existing, err := s.GetBaseByID(ctx, in.ID, category) if err != nil { return nil, &BaseWriteError{Status: http.StatusNotFound, Title: "Not found", Detail: categoryNotFoundDetail(category)} } if existing == nil { return nil, &BaseWriteError{Status: http.StatusNotFound, Title: "Not found", Detail: categoryNotFoundDetail(category)} } prevFotoAttachmentID := append([]byte(nil), existing.FotoAttachmentID...) if in.Latitude != nil && (*in.Latitude < -90 || *in.Latitude > 90) { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "latitude must be between -90 and 90", Pointer: "/data/attributes/latitude"} } if in.Longitude != nil && (*in.Longitude < -180 || *in.Longitude > 180) { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "longitude must be between -180 and 180", Pointer: "/data/attributes/longitude"} } if in.BaseName != nil { v := strings.TrimSpace(*in.BaseName) if v == "" { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "base cannot be empty", Pointer: "/data/attributes/base"} } existing.BaseName = v } if in.BaseAbbreviation != nil { existing.BaseAbbreviation = strings.TrimSpace(*in.BaseAbbreviation) } if in.Address != nil { existing.Address = strings.TrimSpace(*in.Address) } if in.Latitude != nil { existing.Latitude = *in.Latitude } if in.Longitude != nil { existing.Longitude = *in.Longitude } if in.LandlineNumber != nil { existing.LandlineNumber = strings.TrimSpace(*in.LandlineNumber) } if in.MobileNumber != nil { existing.MobileNumber = strings.TrimSpace(*in.MobileNumber) } if in.Email != nil { existing.Email = strings.TrimSpace(*in.Email) } if in.SortKeySet { if in.SortKeyValid { existing.SortKey = cloneIntPtr(in.SortKeyValue) } else { existing.SortKey = nil } } if in.SMSAlert != nil { existing.SMSAlert = *in.SMSAlert } if in.Checklist != nil { existing.Checklist = *in.Checklist } if in.LegTime != nil { existing.LegTime = strings.TrimSpace(*in.LegTime) } if in.DefaultStartTimeType != nil { existing.DefaultStartTimeType = normalizeShiftTimeType(*in.DefaultStartTimeType) } if in.DefaultEndTimeType != nil { existing.DefaultEndTimeType = normalizeShiftTimeType(*in.DefaultEndTimeType) } if in.DefaultShiftStart != nil { existing.DefaultShiftStart = timePtrToClockString(in.DefaultShiftStart) } if in.DefaultShiftEnd != nil { existing.DefaultShiftEnd = timePtrToClockString(in.DefaultShiftEnd) } if normalizeShiftTimeType(existing.DefaultStartTimeType) != base.ShiftTimeTypeFixed { existing.DefaultShiftStart = "" } if normalizeShiftTimeType(existing.DefaultEndTimeType) != base.ShiftTimeTypeFixed { existing.DefaultShiftEnd = "" } if normalizeShiftTimeType(existing.DefaultStartTimeType) == base.ShiftTimeTypeFixed && strings.TrimSpace(existing.DefaultShiftStart) == "" { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_start is required", Pointer: "/data/attributes/default_shift_start"} } if normalizeShiftTimeType(existing.DefaultEndTimeType) == base.ShiftTimeTypeFixed && strings.TrimSpace(existing.DefaultShiftEnd) == "" { return nil, &BaseWriteError{Status: http.StatusUnprocessableEntity, Title: "Validation error", Detail: "default_shift_end is required", Pointer: "/data/attributes/default_shift_end"} } if in.UTC != nil { existing.UTC = normalizeUTCString(*in.UTC) } if in.Dry != nil { existing.Dry = *in.Dry } if in.ControlCenter != nil { existing.ControlCenter = *in.ControlCenter } if in.DUL != nil { existing.DUL = *in.DUL } if in.Notes != nil { existing.Notes = strings.TrimSpace(*in.Notes) } if in.HEMSEDCContactIDsSet { existing.HEMSEDCContactIDs = appendUniqueIDs(in.HEMSEDCContactIDs) } if in.MedPaxContactIDsSet { existing.MedPaxContactIDs = appendUniqueIDs(in.MedPaxContactIDs) } if in.ResponsiblePilotContactIDsSet { existing.ResponsiblePilotContactIDs = appendUniqueIDs(in.ResponsiblePilotContactIDs) } if in.OperationalShiftTimesSet { existing.OperationalShiftTimes = cloneOperationalShiftTimeInputs(in.OperationalShiftTimes) if existing.OperationalShiftTimes == nil { existing.OperationalShiftTimes = []base.BaseOperationalShiftTime{} } } else { existing.OperationalShiftTimes = nil } if in.CreatedBySet { existing.CreatedBy = append([]byte(nil), in.CreatedBy...) } if in.UpdatedBySet { existing.UpdatedBy = append([]byte(nil), in.UpdatedBy...) } if in.FotoAttachmentSet { existing.FotoAttachmentID = append([]byte(nil), in.FotoAttachmentID...) } if in.IsActive != nil { existing.IsActive = *in.IsActive } if err := s.UpdateBase(ctx, existing, category); err != nil { return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()} } updated, err := s.GetBaseByID(ctx, existing.ID, category) if err != nil { return nil, &BaseWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()} } if updated == nil { updated = existing } return &BaseWriteResult{Row: updated, CategoryType: category, PreviousFotoAttachmentID: prevFotoAttachmentID}, nil } func cloneOperationalShiftTimeInputs(items []BaseOperationalShiftTimeInput) []base.BaseOperationalShiftTime { if len(items) == 0 { return nil } out := make([]base.BaseOperationalShiftTime, 0, len(items)) for i := range items { dateStart := items[i].DateStart.UTC() dateEnd := items[i].DateEnd.UTC() out = append(out, base.BaseOperationalShiftTime{ DateStart: &dateStart, DateEnd: &dateEnd, StartTimeType: normalizeShiftTimeType(items[i].StartTimeType), EndTimeType: normalizeShiftTimeType(items[i].EndTimeType), ShiftStart: timePtrToClockString(items[i].ShiftStart), ShiftEnd: timePtrToClockString(items[i].ShiftEnd), }) } return out } func timePtrToClockString(v *time.Time) string { if v == nil { return "" } return v.UTC().Format("15:04:05") } func normalizeUTCString(raw string) string { return strings.TrimSpace(raw) } func normalizeShiftTimeType(raw string) string { switch strings.ToUpper(strings.TrimSpace(raw)) { case "", base.ShiftTimeTypeFixed: return base.ShiftTimeTypeFixed case base.ShiftTimeTypeBMCT: return base.ShiftTimeTypeBMCT case base.ShiftTimeTypeECET: return base.ShiftTimeTypeECET default: return strings.ToUpper(strings.TrimSpace(raw)) } } func appendUniqueIDs(values [][]byte) [][]byte { if len(values) == 0 { return nil } out := make([][]byte, 0, len(values)) seen := map[string]struct{}{} for i := range values { if len(values[i]) != 16 { continue } key := string(values[i]) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, append([]byte(nil), values[i]...)) } return out } func categoryNotFoundDetail(category string) string { if category == base.CategoryKeyHEMS { return "hems base not found" } return "base not found" }