Files
fm_be/internal/service/base_write_service.go
2026-07-16 22:16:45 +07:00

417 lines
16 KiB
Go

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"
}