From c9d20afda8031ec9ab45ad8ae81c8cfa64a39382 Mon Sep 17 00:00:00 2001 From: ats-tech25 Date: Wed, 5 Nov 2025 15:07:28 +0000 Subject: [PATCH] refactor(models): Enhance notification and schedule model structures - Add comprehensive validation and error handling for Notification model - Introduce strongly typed enums for notification types and statuses - Implement GORM hooks for pre-create and pre-update validation - Add helper methods for notification retry and sending logic - Improve Schedule model with validation, availability checks, and duration methods - Add constraints and validation for schedule time slots - Enhance model relationships with foreign key references - Implement additional utility methods for schedule management Improves data integrity, adds robust validation, and provides more comprehensive model behaviors for notifications and schedules. --- internal/models/notification.go | 112 ++++++++++++++++++++++++++++++-- internal/models/schedule.go | 68 +++++++++++++++++-- 2 files changed, 169 insertions(+), 11 deletions(-) diff --git a/internal/models/notification.go b/internal/models/notification.go index 261ab41..f17e86b 100644 --- a/internal/models/notification.go +++ b/internal/models/notification.go @@ -1,19 +1,117 @@ package models import ( + "errors" "time" "gorm.io/gorm" ) +// NotificationType represents the different types of notifications +type NotificationType string + +const ( + NotificationTypeWelcome NotificationType = "welcome" + NotificationTypePaymentSuccess NotificationType = "payment_success" + NotificationTypePaymentFailed NotificationType = "payment_failed" + NotificationTypeMeetingInfo NotificationType = "meeting_info" + NotificationTypeReminder NotificationType = "reminder" + NotificationTypeCancellation NotificationType = "cancellation" + NotificationTypeReschedule NotificationType = "reschedule" +) + +// NotificationStatus represents the status of a notification +type NotificationStatus string + +const ( + NotificationStatusPending NotificationStatus = "pending" + NotificationStatusSent NotificationStatus = "sent" + NotificationStatusFailed NotificationStatus = "failed" + NotificationStatusSkipped NotificationStatus = "skipped" +) + // Notification represents email notifications type Notification struct { gorm.Model - UserID uint `json:"user_id"` - Type string `json:"type"` // welcome, payment_success, payment_failed, meeting_info, reminder - Subject string `json:"subject"` - Body string `json:"body"` - SentAt *time.Time `json:"sent_at"` - Status string `json:"status" gorm:"default:'pending'"` // pending, sent, failed - ScheduledAt *time.Time `json:"scheduled_at"` + UserID uint `json:"user_id" gorm:"index" validate:"required"` + User User `json:"user" gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + BookingID *uint `json:"booking_id" gorm:"index"` + Booking *Booking `json:"booking" gorm:"foreignKey:BookingID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` + Type NotificationType `json:"type" gorm:"not null;size:50;index" validate:"required,oneof=welcome payment_success payment_failed meeting_info reminder cancellation reschedule"` + Subject string `json:"subject" gorm:"not null;size:255" validate:"required,max=255"` + Body string `json:"body" gorm:"type:text" validate:"required"` + SentAt *time.Time `json:"sent_at" gorm:"index"` + Status NotificationStatus `json:"status" gorm:"default:'pending';size:20;index" validate:"required,oneof=pending sent failed skipped"` + ScheduledAt *time.Time `json:"scheduled_at" gorm:"index"` + RetryCount int `json:"retry_count" gorm:"default:0;check:retry_count >= 0"` + ErrorMsg string `json:"error_msg" gorm:"size:500"` +} + +// BeforeCreate is a GORM hook that runs before creating a notification record +func (n *Notification) BeforeCreate(tx *gorm.DB) error { + // Set default status if not provided + if n.Status == "" { + n.Status = NotificationStatusPending + } + + // Validate required fields + if n.Subject == "" { + return errors.New("notification subject is required") + } + + if n.Body == "" { + return errors.New("notification body is required") + } + + // If scheduled at is not set, schedule for immediate sending + if n.ScheduledAt == nil { + now := time.Now() + n.ScheduledAt = &now + } + + return nil +} + +// BeforeUpdate is a GORM hook that runs before updating a notification record +func (n *Notification) BeforeUpdate(tx *gorm.DB) error { + // If status is being set to sent, set SentAt timestamp + if n.Status == NotificationStatusSent && n.SentAt == nil { + now := time.Now() + n.SentAt = &now + } + + return nil +} + +// IsReadyToSend checks if the notification is ready to be sent +func (n *Notification) IsReadyToSend() bool { + if n.Status != NotificationStatusPending { + return false + } + + if n.ScheduledAt == nil { + return true + } + + return n.ScheduledAt.Before(time.Now()) || n.ScheduledAt.Equal(time.Now()) +} + +// CanRetry checks if the notification can be retried (max 3 retries) +func (n *Notification) CanRetry() bool { + return n.Status == NotificationStatusFailed && n.RetryCount < 3 +} + +// MarkAsSent marks the notification as successfully sent +func (n *Notification) MarkAsSent() { + n.Status = NotificationStatusSent + now := time.Now() + n.SentAt = &now + n.ErrorMsg = "" +} + +// MarkAsFailed marks the notification as failed with an error message +func (n *Notification) MarkAsFailed(errorMsg string) { + n.Status = NotificationStatusFailed + n.RetryCount++ + n.ErrorMsg = errorMsg } diff --git a/internal/models/schedule.go b/internal/models/schedule.go index c05d3e3..1e2cdab 100644 --- a/internal/models/schedule.go +++ b/internal/models/schedule.go @@ -1,6 +1,7 @@ package models import ( + "errors" "time" "gorm.io/gorm" @@ -9,9 +10,68 @@ import ( // Schedule represents available time slots type Schedule struct { gorm.Model - StartTime time.Time `json:"start_time" gorm:"not null"` - EndTime time.Time `json:"end_time" gorm:"not null"` + StartTime time.Time `json:"start_time" gorm:"not null;index" validate:"required"` + EndTime time.Time `json:"end_time" gorm:"not null;index" validate:"required"` IsAvailable bool `json:"is_available" gorm:"default:true"` - MaxBookings int `json:"max_bookings" gorm:"default:1"` - BookedCount int `json:"booked_count" gorm:"default:0"` + MaxBookings int `json:"max_bookings" gorm:"default:1;check:max_bookings > 0" validate:"min=1,max=100"` + BookedCount int `json:"booked_count" gorm:"default:0;check:booked_count >= 0" validate:"min=0"` +} + +// BeforeCreate is a GORM hook that runs before creating a schedule record +func (s *Schedule) BeforeCreate(tx *gorm.DB) error { + // Validate that end time is after start time + if !s.EndTime.After(s.StartTime) { + return errors.New("end time must be after start time") + } + + // Validate minimum duration (15 minutes) + if s.EndTime.Sub(s.StartTime) < 15*time.Minute { + return errors.New("schedule slot must be at least 15 minutes long") + } + + // Validate that start time is in the future + if s.StartTime.Before(time.Now()) { + return errors.New("schedule slot cannot be created in the past") + } + + // Set default values + if s.MaxBookings == 0 { + s.MaxBookings = 1 + } + + return nil +} + +// BeforeUpdate is a GORM hook that runs before updating a schedule record +func (s *Schedule) BeforeUpdate(tx *gorm.DB) error { + // Validate that end time is after start time + if !s.EndTime.After(s.StartTime) { + return errors.New("end time must be after start time") + } + + // Validate that booked count doesn't exceed max bookings + if s.BookedCount > s.MaxBookings { + return errors.New("booked count cannot exceed max bookings") + } + + return nil +} + +// IsAvailableForBooking checks if the schedule slot has availability +func (s *Schedule) IsAvailableForBooking() bool { + return s.IsAvailable && s.BookedCount < s.MaxBookings && s.StartTime.After(time.Now()) +} + +// GetRemainingSlots returns the number of remaining booking slots +func (s *Schedule) GetRemainingSlots() int { + remaining := s.MaxBookings - s.BookedCount + if remaining < 0 { + return 0 + } + return remaining +} + +// Duration returns the duration of the schedule slot +func (s *Schedule) Duration() time.Duration { + return s.EndTime.Sub(s.StartTime) }