init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,569 @@
package dto
import (
"context"
"strings"
"time"
"wucher/internal/domain/base"
"wucher/internal/shared/pkg/userctx"
"wucher/internal/shared/pkg/uuidv7"
)
// JSON:API Base DTOs
type BaseAttributes struct {
BaseName string `json:"base" example:"Lude"`
BaseAbbreviation string `json:"base_abbreviation,omitempty" example:"LOIG"`
BaseCategory string `json:"base_category,omitempty" example:"regular" enums:"regular,hems"`
Address string `json:"address,omitempty" example:"Lude Airfield"`
Latitude float64 `json:"latitude" example:"47.3769"`
Longitude float64 `json:"longitude" example:"8.5417"`
LandlineNumber string `json:"landline_number,omitempty" example:"+62-21-1234"`
MobileNumber string `json:"mobile_number,omitempty" example:"+62-812-1234"`
Email string `json:"email,omitempty" example:"lude@wucher.local"`
SortKey *int `json:"sortkey" example:"1"`
SMSAlert bool `json:"sms_alert" example:"false"`
Checklist bool `json:"checklist" example:"false"`
LegTime string `json:"leg_time,omitempty" example:"00:20"`
DefaultStartTimeType string `json:"default_start_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
DefaultEndTimeType string `json:"default_end_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
ShiftTime BaseShiftTime `json:"shift_time"`
UTC string `json:"utc" example:"0"`
Foto *BaseFoto `json:"foto,omitempty"`
Dry bool `json:"dry" example:"false"`
ControlCenter bool `json:"control_center" example:"false"`
DUL bool `json:"dul" example:"false"`
Notes string `json:"notes,omitempty" example:"Main base"`
HEMSEDCContactIDs []string `json:"hems_edc_contact_ids,omitempty"`
HEMSEDCs []BaseContact `json:"hems_edc,omitempty"`
MedPaxContactIDs []string `json:"med_pax_contact_ids,omitempty"`
MedPax []BaseContact `json:"med_pax,omitempty"`
ResponsiblePilotContactIDs []string `json:"responsible_pilot_contact_ids,omitempty"`
ResponsiblePilots []BaseContact `json:"responsible_pilots,omitempty"`
OperationalShiftTimes []BaseOperationalShiftTime `json:"operational_shift_times,omitempty"`
IsActive bool `json:"is_active" example:"true"`
CreatedBy string `json:"created_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"`
UpdatedBy string `json:"updated_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"`
CreatedAt string `json:"created_at,omitempty" example:"2026-03-12T03:04:05Z"`
UpdatedAt string `json:"updated_at,omitempty" example:"2026-03-12T03:04:05Z"`
DeletedAt string `json:"deleted_at,omitempty" example:"2026-03-12T03:04:05Z" swaggerignore:"true"`
DeletedBy string `json:"deleted_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d" swaggerignore:"true"`
}
type BaseContact struct {
ID string `json:"id"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
}
type BaseShiftTime struct {
Start string `json:"start,omitempty" example:"06:00"`
End string `json:"end,omitempty" example:"21:00"`
}
type BaseOperationalShiftTime struct {
DateStart string `json:"date_start" example:"2026-04-12"`
DateEnd string `json:"date_end" example:"2026-04-12"`
StartTimeType string `json:"start_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
EndTimeType string `json:"end_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
ShiftTime BaseShiftTime `json:"shift_time"`
}
type BaseFoto struct {
UUID string `json:"uuid" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"`
DownloadURL string `json:"download_url" example:"https://storage.example.com/base/lude.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=abc123"`
ThumbnailDownloadURL string `json:"thumbnail_download_url,omitempty" example:"https://storage.example.com/base/lude.thumb.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=abc123"`
OriginalSizeBytes *int64 `json:"original_size_bytes,omitempty" example:"1024000"`
}
type BaseResource struct {
Type string `json:"type" example:"base"`
ID string `json:"id" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"`
Attributes BaseAttributes `json:"attributes"`
}
type BaseResponse struct {
Data BaseResource `json:"data"`
}
type BaseListResponse struct {
Data []BaseResource `json:"data"`
Meta struct {
PageNumber int `json:"page_number" example:"1"`
PageSize int `json:"page_size" example:"20"`
Total int64 `json:"total" example:"120"`
} `json:"meta"`
}
type BaseDataTableResponse struct {
Data []BaseResource `json:"data"`
Meta struct {
Draw int `json:"draw" example:"1"`
RecordsTotal int64 `json:"records_total" example:"120"`
RecordsFiltered int64 `json:"records_filtered" example:"12"`
} `json:"meta"`
}
type BaseCreateAttributes struct {
BaseName string `json:"base" validate:"required" example:"Lude"`
BaseAbbreviation *string `json:"base_abbreviation,omitempty" example:"LOIG"`
BaseCategory *string `json:"base_category,omitempty" example:"regular" enums:"regular,hems"`
Address *string `json:"address,omitempty" example:"Lude Airfield"`
Latitude *float64 `json:"latitude" validate:"required,gte=-90,lte=90" example:"47.3769"`
Longitude *float64 `json:"longitude" validate:"required,gte=-180,lte=180" example:"8.5417"`
LandlineNumber *string `json:"landline_number,omitempty" example:"+62-21-1234"`
MobileNumber *string `json:"mobile_number,omitempty" example:"+62-812-1234"`
Email *string `json:"email,omitempty" example:"lude@wucher.local"`
SortKey *int `json:"sortkey" example:"1"`
SMSAlert *bool `json:"sms_alert,omitempty" example:"false"`
Checklist *bool `json:"checklist,omitempty" example:"false"`
LegTime *string `json:"leg_time,omitempty" example:"00:20"`
DefaultStartTimeType *string `json:"default_start_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
DefaultEndTimeType *string `json:"default_end_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
ShiftTime *BaseShiftTime `json:"shift_time,omitempty"`
DefaultShiftStart *string `json:"default_shift_start,omitempty" example:"06:00:00"`
DefaultShiftEnd *string `json:"default_shift_end,omitempty" example:"21:00:00"`
DefaultShiftTime *string `json:"default_shift_time,omitempty" example:"12:00"`
UTC *string `json:"utc,omitempty" example:"0"`
FileUUID *string `json:"file_uuid,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e90"`
Dry *bool `json:"dry,omitempty" example:"false"`
ControlCenter *bool `json:"control_center,omitempty" example:"false"`
DUL *bool `json:"dul,omitempty" example:"false"`
Notes *string `json:"notes,omitempty" example:"Main base"`
HEMSEDCContactIDs []string `json:"hems_edc_contact_ids,omitempty"`
MedPaxContactIDs []string `json:"med_pax_contact_ids,omitempty"`
ResponsiblePilotContactIDs []string `json:"responsible_pilot_contact_ids,omitempty"`
OperationalShiftTimes []BaseOperationalShiftTime `json:"operational_shift_times,omitempty"`
IsActive *bool `json:"is_active,omitempty" example:"true"`
CreatedBy *string `json:"created_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d" swaggerignore:"true"`
UpdatedBy *string `json:"updated_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d" swaggerignore:"true"`
}
type BaseCreateData struct {
Type string `json:"type" validate:"required,oneof=base_create base hems_base" enums:"base" example:"base"`
Attributes BaseCreateAttributes `json:"attributes" validate:"required"`
}
type BaseCreateRequest struct {
Data BaseCreateData `json:"data" validate:"required"`
}
type BaseUpdateAttributes struct {
BaseName *string `json:"base,omitempty" example:"Lude"`
BaseAbbreviation *string `json:"base_abbreviation,omitempty" example:"LOIG"`
BaseCategory *string `json:"base_category,omitempty" example:"regular" enums:"regular,hems"`
Address *string `json:"address,omitempty" example:"Lude Airfield"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,gte=-90,lte=90" example:"47.3769"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,gte=-180,lte=180" example:"8.5417"`
LandlineNumber *string `json:"landline_number,omitempty" example:"+62-21-1234"`
MobileNumber *string `json:"mobile_number,omitempty" example:"+62-812-1234"`
Email *string `json:"email,omitempty" example:"lude@wucher.local"`
SortKey NullInt `json:"sortkey,omitempty" swaggertype:"integer" example:"1"`
SMSAlert *bool `json:"sms_alert,omitempty" example:"false"`
Checklist *bool `json:"checklist,omitempty" example:"false"`
LegTime *string `json:"leg_time,omitempty" example:"00:20"`
DefaultStartTimeType *string `json:"default_start_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
DefaultEndTimeType *string `json:"default_end_time_type,omitempty" example:"FIXED" enums:"FIXED,BMCT,ECET"`
DefaultShiftStart *string `json:"default_shift_start,omitempty" example:"06:00:00"`
DefaultShiftEnd *string `json:"default_shift_end,omitempty" example:"21:00:00"`
DefaultShiftTime *string `json:"default_shift_time,omitempty" example:"12:00"`
ShiftTime *BaseShiftTime `json:"shift_time,omitempty"`
UTC *string `json:"utc,omitempty" example:"0"`
FileUUID *string `json:"file_uuid,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e90"`
Dry *bool `json:"dry,omitempty" example:"false"`
ControlCenter *bool `json:"control_center,omitempty" example:"false"`
DUL *bool `json:"dul,omitempty" example:"false"`
Notes *string `json:"notes,omitempty" example:"Main base"`
HEMSEDCContactIDs *[]string `json:"hems_edc_contact_ids,omitempty"`
MedPaxContactIDs *[]string `json:"med_pax_contact_ids,omitempty"`
ResponsiblePilotContactIDs *[]string `json:"responsible_pilot_contact_ids,omitempty"`
OperationalShiftTimes []BaseOperationalShiftTime `json:"operational_shift_times,omitempty"`
IsActive *bool `json:"is_active,omitempty" example:"true"`
CreatedBy *string `json:"created_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d" swaggerignore:"true"`
UpdatedBy *string `json:"updated_by,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d" swaggerignore:"true"`
}
type BaseUpdateData struct {
Type string `json:"type" validate:"required,oneof=base_update base hems_base" enums:"base_update" example:"base_update"`
ID string `json:"id,omitempty" example:"018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"`
Attributes BaseUpdateAttributes `json:"attributes" validate:"required"`
}
type BaseUpdateRequest struct {
Data BaseUpdateData `json:"data" validate:"required"`
}
type BaseDeleteAttributes struct {
Deleted bool `json:"deleted" example:"true"`
}
type BaseDeleteResource struct {
Type string `json:"type" example:"base_delete"`
ID string `json:"id" example:"019d6b10-a385-7e9e-a553-8e892bcec0ef"`
Attributes BaseDeleteAttributes `json:"attributes"`
}
type BaseDeleteResponse struct {
Data BaseDeleteResource `json:"data"`
}
type BaseResponseMappingInput struct {
ResourceType string
BaseCategory string
ID string
DefaultStartTimeType string
DefaultEndTimeType string
ShiftTime BaseShiftTime
Foto *BaseFoto
Latitude float64
Longitude float64
HEMSEDCContactIDs []string
HEMSEDCs []BaseContact
MedPaxContactIDs []string
MedPax []BaseContact
ResponsiblePilotContactIDs []string
ResponsiblePilots []BaseContact
OperationalShiftTimes []BaseOperationalShiftTime
}
func MapBaseCreateAttributesToDomain(attrs BaseCreateAttributes) *base.Base {
row := &base.Base{}
row.BaseName = strings.TrimSpace(attrs.BaseName)
row.BaseAbbreviation = strings.TrimSpace(stringOrDefault(attrs.BaseAbbreviation, ""))
row.Address = strings.TrimSpace(stringOrDefault(attrs.Address, ""))
row.Latitude = floatOrDefault(attrs.Latitude, 0)
row.Longitude = floatOrDefault(attrs.Longitude, 0)
row.LandlineNumber = strings.TrimSpace(stringOrDefault(attrs.LandlineNumber, ""))
row.MobileNumber = strings.TrimSpace(stringOrDefault(attrs.MobileNumber, ""))
row.Email = strings.TrimSpace(stringOrDefault(attrs.Email, ""))
row.SortKey = cloneIntPtr(attrs.SortKey)
row.SMSAlert = boolOrDefault(attrs.SMSAlert, false)
row.Checklist = boolOrDefault(attrs.Checklist, false)
row.LegTime = strings.TrimSpace(stringOrDefault(attrs.LegTime, ""))
row.DefaultStartTimeType = normalizeShiftTimeType(stringOrDefault(attrs.DefaultStartTimeType, ""))
row.DefaultEndTimeType = normalizeShiftTimeType(stringOrDefault(attrs.DefaultEndTimeType, ""))
row.UTC = normalizeUTC(attrs.UTC)
row.Dry = boolOrDefault(attrs.Dry, false)
row.ControlCenter = boolOrDefault(attrs.ControlCenter, false)
row.DUL = boolOrDefault(attrs.DUL, false)
row.Notes = strings.TrimSpace(stringOrDefault(attrs.Notes, ""))
row.IsActive = boolOrDefault(attrs.IsActive, true)
row.OperationalShiftTimes = mapBaseOperationalShiftTimesToDomain(attrs.OperationalShiftTimes)
return row
}
func ApplyBaseUpdateAttributesToDomain(row *base.Base, attrs BaseUpdateAttributes) {
if attrs.BaseName != nil {
row.BaseName = strings.TrimSpace(*attrs.BaseName)
}
if attrs.BaseAbbreviation != nil {
row.BaseAbbreviation = strings.TrimSpace(*attrs.BaseAbbreviation)
}
if attrs.Address != nil {
row.Address = strings.TrimSpace(*attrs.Address)
}
if attrs.Latitude != nil {
row.Latitude = *attrs.Latitude
}
if attrs.Longitude != nil {
row.Longitude = *attrs.Longitude
}
if attrs.LandlineNumber != nil {
row.LandlineNumber = strings.TrimSpace(*attrs.LandlineNumber)
}
if attrs.MobileNumber != nil {
row.MobileNumber = strings.TrimSpace(*attrs.MobileNumber)
}
if attrs.Email != nil {
row.Email = strings.TrimSpace(*attrs.Email)
}
if attrs.SortKey.Set {
if attrs.SortKey.Valid {
v := attrs.SortKey.Value
row.SortKey = &v
} else {
row.SortKey = nil
}
}
if attrs.SMSAlert != nil {
row.SMSAlert = *attrs.SMSAlert
}
if attrs.Checklist != nil {
row.Checklist = *attrs.Checklist
}
if attrs.LegTime != nil {
row.LegTime = strings.TrimSpace(*attrs.LegTime)
}
if attrs.DefaultStartTimeType != nil {
row.DefaultStartTimeType = normalizeShiftTimeType(*attrs.DefaultStartTimeType)
}
if attrs.DefaultEndTimeType != nil {
row.DefaultEndTimeType = normalizeShiftTimeType(*attrs.DefaultEndTimeType)
}
if attrs.UTC != nil {
row.UTC = normalizeUTC(attrs.UTC)
}
if attrs.Dry != nil {
row.Dry = *attrs.Dry
}
if attrs.ControlCenter != nil {
row.ControlCenter = *attrs.ControlCenter
}
if attrs.DUL != nil {
row.DUL = *attrs.DUL
}
if attrs.Notes != nil {
row.Notes = strings.TrimSpace(*attrs.Notes)
}
if attrs.IsActive != nil {
row.IsActive = *attrs.IsActive
}
}
func mapBaseOperationalShiftTimesToDomain(items []BaseOperationalShiftTime) []base.BaseOperationalShiftTime {
if len(items) == 0 {
return nil
}
out := make([]base.BaseOperationalShiftTime, 0, len(items))
for i := range items {
dateStart := parseBaseOperationDatePtr(items[i].DateStart)
dateEnd := parseBaseOperationDatePtr(items[i].DateEnd)
out = append(out, base.BaseOperationalShiftTime{
DateStart: dateStart,
DateEnd: dateEnd,
StartTimeType: normalizeShiftTimeType(items[i].StartTimeType),
EndTimeType: normalizeShiftTimeType(items[i].EndTimeType),
ShiftStart: normalizeBaseClockValue(items[i].ShiftTime.Start),
ShiftEnd: normalizeBaseClockValue(items[i].ShiftTime.End),
})
}
return out
}
func MapBaseOperationalShiftTimesToDTO(items []base.BaseOperationalShiftTime) []BaseOperationalShiftTime {
if len(items) == 0 {
return nil
}
out := make([]BaseOperationalShiftTime, 0, len(items))
for i := range items {
out = append(out, BaseOperationalShiftTime{
DateStart: timePtrToDateString(items[i].DateStart),
DateEnd: timePtrToDateString(items[i].DateEnd),
StartTimeType: normalizeShiftTimeType(items[i].StartTimeType),
EndTimeType: normalizeShiftTimeType(items[i].EndTimeType),
ShiftTime: BaseShiftTime{
Start: DisplayBaseShiftValue(items[i].StartTimeType, items[i].ShiftStart),
End: DisplayBaseShiftValue(items[i].EndTimeType, items[i].ShiftEnd),
},
})
}
return out
}
func parseBaseOperationDate(raw string) time.Time {
parsed, err := time.Parse("2006-01-02", strings.TrimSpace(raw))
if err != nil {
return time.Time{}
}
return parsed.UTC()
}
func parseBaseOperationDatePtr(raw string) *time.Time {
parsed := parseBaseOperationDate(raw)
if parsed.IsZero() {
return nil
}
return &parsed
}
func timePtrToDateString(v *time.Time) string {
if v == nil || v.IsZero() {
return ""
}
return v.UTC().Format("2006-01-02")
}
func normalizeBaseClockValue(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
if len(trimmed) == 5 {
trimmed += ":00"
}
parsed, err := time.Parse("15:04:05", trimmed)
if err != nil {
return ""
}
return parsed.UTC().Format("15:04:05")
}
func normalizeBaseShiftDisplay(raw string) string {
s := strings.TrimSpace(raw)
if s == "" {
return s
}
layouts := []string{
"15:04:05",
"15:04",
"2006-01-02 15:04:05",
"2006-01-02 15:04",
time.RFC3339,
time.RFC3339Nano,
}
for _, layout := range layouts {
if parsed, err := time.Parse(layout, s); err == nil {
return parsed.UTC().Format("15:04")
}
}
if len(s) >= 5 && isDigitBaseClock(s[0]) && isDigitBaseClock(s[1]) && s[2] == ':' && isDigitBaseClock(s[3]) && isDigitBaseClock(s[4]) {
return s[:5]
}
return s
}
// DisplayBaseShiftValue returns the user-facing representation for a shift value.
// Fixed shifts are shown as HH:MM, while BMCT/ECET are shown as labels.
func DisplayBaseShiftValue(timeType, raw string) string {
switch normalizeShiftTimeType(timeType) {
case "BMCT", "ECET":
return normalizeShiftTimeType(timeType)
default:
return normalizeBaseShiftDisplay(raw)
}
}
func normalizeShiftTimeType(raw string) string {
switch strings.ToUpper(strings.TrimSpace(raw)) {
case "", "FIXED":
return "FIXED"
case "BMCT":
return "BMCT"
case "ECET":
return "ECET"
default:
return strings.ToUpper(strings.TrimSpace(raw))
}
}
func isDigitBaseClock(b byte) bool {
return b >= '0' && b <= '9'
}
func MapBaseToResource(row *base.Base, in BaseResponseMappingInput) BaseResource {
if row == nil {
return BaseResource{
Type: in.ResourceType,
Attributes: BaseAttributes{
BaseCategory: in.BaseCategory,
DefaultStartTimeType: normalizeShiftTimeType(in.DefaultStartTimeType),
DefaultEndTimeType: normalizeShiftTimeType(in.DefaultEndTimeType),
ShiftTime: in.ShiftTime,
OperationalShiftTimes: in.OperationalShiftTimes,
Latitude: in.Latitude,
Longitude: in.Longitude,
},
}
}
return BaseResource{
Type: in.ResourceType,
ID: in.ID,
Attributes: BaseAttributes{
BaseName: row.BaseName,
BaseAbbreviation: row.BaseAbbreviation,
BaseCategory: in.BaseCategory,
Address: row.Address,
Latitude: row.Latitude,
Longitude: row.Longitude,
LandlineNumber: row.LandlineNumber,
MobileNumber: row.MobileNumber,
Email: row.Email,
SortKey: cloneIntPtr(row.SortKey),
SMSAlert: row.SMSAlert,
Checklist: row.Checklist,
LegTime: row.LegTime,
DefaultStartTimeType: normalizeShiftTimeType(row.DefaultStartTimeType),
DefaultEndTimeType: normalizeShiftTimeType(row.DefaultEndTimeType),
ShiftTime: in.ShiftTime,
UTC: row.UTC,
Foto: in.Foto,
Dry: row.Dry,
ControlCenter: row.ControlCenter,
DUL: row.DUL,
Notes: row.Notes,
HEMSEDCContactIDs: in.HEMSEDCContactIDs,
HEMSEDCs: in.HEMSEDCs,
MedPaxContactIDs: in.MedPaxContactIDs,
MedPax: in.MedPax,
ResponsiblePilotContactIDs: in.ResponsiblePilotContactIDs,
ResponsiblePilots: in.ResponsiblePilots,
OperationalShiftTimes: in.OperationalShiftTimes,
IsActive: row.IsActive,
CreatedBy: bytesToUUID(row.CreatedBy),
UpdatedBy: bytesToUUID(row.UpdatedBy),
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
DeletedAt: timePtrToString(row.DeletedAt),
DeletedBy: bytesToUUID(row.DeletedBy),
},
}
}
func stringOrDefault(v *string, d string) string {
if v == nil {
return d
}
return *v
}
func boolOrDefault(v *bool, d bool) bool {
if v == nil {
return d
}
return *v
}
func normalizeUTC(v *string) string {
s := strings.TrimSpace(stringOrDefault(v, "0"))
if s == "" {
return "0"
}
return s
}
func floatOrDefault(v *float64, d float64) float64 {
if v == nil {
return d
}
return *v
}
func cloneIntPtr(v *int) *int {
if v == nil {
return nil
}
x := *v
return &x
}
func bytesToUUID(v []byte) string {
if len(v) == 0 {
return ""
}
if name := userctx.GetDisplayName(context.TODO(), v); name != "" {
return name
}
s, err := uuidv7.BytesToString(v)
if err != nil {
return ""
}
return s
}
func timePtrToString(v *time.Time) string {
if v == nil {
return ""
}
return v.UTC().Format(time.RFC3339)
}