package handlers import ( "context" "database/sql" "errors" "testing" "time" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" ) // Mocks type mockQuoteRow struct { reminderType int32 reminderSent time.Time designatedDay time.Weekday emailSent bool } func (m *mockQuoteRow) GetReminderType() int32 { return m.reminderType } func (m *mockQuoteRow) GetReminderSent() time.Time { return m.reminderSent } func (m *mockQuoteRow) GetCustomerEmail() string { return "test@example.com" } // Realistic mock for db.Queries type mockQueries struct { reminders []db.QuoteReminder insertCalled bool } func (m *mockQueries) GetQuoteRemindersByType(ctx context.Context, params db.GetQuoteRemindersByTypeParams) ([]db.QuoteReminder, error) { var filtered []db.QuoteReminder for _, r := range m.reminders { if r.QuoteID == params.QuoteID && r.ReminderType == params.ReminderType { filtered = append(filtered, r) } } return filtered, nil } func (m *mockQueries) InsertQuoteReminder(ctx context.Context, params db.InsertQuoteReminderParams) (sql.Result, error) { m.insertCalled = true m.reminders = append(m.reminders, db.QuoteReminder{ QuoteID: params.QuoteID, ReminderType: params.ReminderType, DateSent: params.DateSent, Username: params.Username, }) return &mockResult{}, nil } func (m *mockQueries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesRow, error) { return nil, nil } func (m *mockQueries) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesRow, error) { return nil, nil } func (m *mockQueries) GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesOnDayRow, error) { return nil, nil } func (m *mockQueries) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesOnDayRow, error) { return nil, nil } // Mock sql.Result for InsertQuoteReminder type mockResult struct{} func (m *mockResult) LastInsertId() (int64, error) { return 1, nil } func (m *mockResult) RowsAffected() (int64, error) { return 1, nil } // Realistic mock for email.EmailService type mockEmailService struct { sent bool sentReminders map[int32]map[int32]bool // quoteID -> reminderType -> sent } func (m *mockEmailService) SendTemplateEmail(to, subject, templateName string, data interface{}, ccs, bccs []string) error { m.sent = true if m.sentReminders == nil { m.sentReminders = make(map[int32]map[int32]bool) } var quoteID int32 var reminderType int32 if dataMap, ok := data.(map[string]interface{}); ok { if id, ok := dataMap["QuoteID"].(int32); ok { quoteID = id } else if id, ok := dataMap["QuoteID"].(int); ok { quoteID = int32(id) } if rt, ok := dataMap["ReminderType"].(int32); ok { reminderType = rt } else if rt, ok := dataMap["ReminderType"].(int); ok { reminderType = int32(rt) } } if quoteID == 0 { quoteID = 123 } if reminderType == 0 { reminderType = 1 } if m.sentReminders[quoteID] == nil { m.sentReminders[quoteID] = make(map[int32]bool) } m.sentReminders[quoteID][reminderType] = true return nil } // Mock for db.Queries with error simulation type mockQueriesError struct{} func (m *mockQueriesError) GetQuoteRemindersByType(ctx context.Context, params db.GetQuoteRemindersByTypeParams) ([]db.QuoteReminder, error) { return nil, errors.New("db error") } func (m *mockQueriesError) InsertQuoteReminder(ctx context.Context, params db.InsertQuoteReminderParams) (sql.Result, error) { return &mockResult{}, nil } func (m *mockQueriesError) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesRow, error) { return nil, nil } func (m *mockQueriesError) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesRow, error) { return nil, nil } func (m *mockQueriesError) GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesOnDayRow, error) { return nil, nil } func (m *mockQueriesError) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesOnDayRow, error) { return nil, nil } // Test: Should send email on designated day if not already sent // Description: Verifies that a reminder email is sent and recorded when no previous reminder exists for the quote. Should pass unless the handler logic is broken. func TestSendQuoteReminderEmail_OnDesignatedDay(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} me := &mockEmailService{} h := &QuotesHandler{ queries: mq, emailService: me, } // Simulate designated day logic by calling SendQuoteReminderEmail err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) if err != nil { t.Fatalf("Expected no error, got %v", err) } if !me.sent { t.Error("Expected email to be sent on designated day") } if !mq.insertCalled { t.Error("Expected reminder to be recorded on designated day") } } // Test: Should NOT send email if reminder already sent // Description: Verifies that if a reminder for the quote and type already exists, the handler does not send another email or record a duplicate. Should fail if duplicate reminders are allowed. func TestSendQuoteReminderEmail_AlreadyReminded(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{{QuoteID: 123, ReminderType: 1}}} me := &mockEmailService{} h := &QuotesHandler{ queries: mq, emailService: me, } err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) if err == nil { t.Error("Expected error for already sent reminder") } if me.sent { t.Error("Expected email NOT to be sent if already reminded") } if mq.insertCalled { t.Error("Expected reminder NOT to be recorded if already reminded") } } // Test: Should only send reminder once, second call should fail // Description: Sends a reminder, then tries to send the same reminder again. The first should succeed, the second should fail and not send or record a duplicate. Should fail if duplicates are allowed. func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} me := &mockEmailService{} h := &QuotesHandler{ queries: mq, emailService: me, } // First call should succeed err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) if err1 != nil { t.Fatalf("Expected first call to succeed, got %v", err1) } if !me.sentReminders[123][1] { t.Error("Expected email to be sent and recorded for quote 123, reminder 1") } if len(mq.reminders) != 1 { t.Errorf("Expected 1 reminder recorded in DB, got %d", len(mq.reminders)) } // Second call should fail err2 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) if err2 == nil { t.Error("Expected error for already sent reminder on second call") } if len(mq.reminders) != 1 { t.Errorf("Expected no additional reminder recorded in DB, got %d", len(mq.reminders)) } } // Test: Should send and record reminder if not already sent // Description: Verifies that a reminder is sent and recorded when no previous reminder exists. Should pass unless the handler logic is broken. func TestSendQuoteReminderEmail_SendsIfNotAlreadySent(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} me := &mockEmailService{} h := &QuotesHandler{ queries: mq, emailService: me, } err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) if err != nil { t.Fatalf("Expected no error, got %v", err) } if !me.sent { t.Error("Expected email to be sent") } if !mq.insertCalled { t.Error("Expected reminder to be recorded") } } // Test: Should ignore already sent reminder // Description: Verifies that the handler does not send or record a reminder if one already exists for the quote/type. Should fail if duplicates are allowed. func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{{QuoteID: 123, ReminderType: 1}}} me := &mockEmailService{} h := &QuotesHandler{ queries: mq, emailService: me, } err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) if err == nil { t.Error("Expected error for already sent reminder") } if me.sent { t.Error("Expected email NOT to be sent") } if mq.insertCalled { t.Error("Expected reminder NOT to be recorded") } } // Test: Should fail if DB returns error // Description: Simulates a DB error and expects the handler to return an error. Should fail if DB errors are not handled. func TestSendQuoteReminderEmail_DBError(t *testing.T) { h := &QuotesHandler{queries: &mockQueriesError{}} err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) if err == nil { t.Error("Expected error when DB fails, got nil") } } // Edge case: nil recipient // Test: Should fail if recipient is empty // Description: Verifies that the handler returns an error if the recipient email is missing. Should fail if emails are sent to empty recipients. func TestSendQuoteReminderEmail_NilRecipient(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} h := &QuotesHandler{queries: mq} err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "", "Subject", "template", map[string]interface{}{}, nil) if err == nil { t.Error("Expected error for nil recipient, got nil") } } // Edge case: missing template data // Test: Should fail if template data is missing // Description: Verifies that the handler returns an error if template data is nil. Should fail if emails are sent without template data. func TestSendQuoteReminderEmail_MissingTemplateData(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} h := &QuotesHandler{queries: mq} err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", nil, nil) if err == nil { t.Error("Expected error for missing template data, got nil") } } // Boundary: invalid reminder type // Test: Should fail if reminder type is invalid // Description: Verifies that the handler returns an error for an invalid reminder type. Should fail if invalid types are allowed. func TestSendQuoteReminderEmail_InvalidReminderType(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} h := &QuotesHandler{queries: mq} err := h.SendQuoteReminderEmail(context.Background(), 123, 99, "test@example.com", "Subject", "template", map[string]interface{}{}, nil) if err == nil { t.Error("Expected error for invalid reminder type, got nil") } } // Test: Multiple reminders of different types allowed for same quote // Description: Verifies that reminders of different types for the same quote can be sent and recorded independently. Should fail if only one reminder per quote is allowed. func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} me := &mockEmailService{} h := &QuotesHandler{queries: mq, emailService: me} // First reminder type err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) if err1 != nil { t.Fatalf("Expected first reminder to be sent, got %v", err1) } if !me.sentReminders[123][1] { t.Error("Expected email to be sent and recorded for quote 123, reminder 1") } if len(mq.reminders) != 1 { t.Errorf("Expected 1 reminder recorded in DB after first send, got %d", len(mq.reminders)) } // Second reminder type err2 := h.SendQuoteReminderEmail(context.Background(), 123, 2, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 2}, nil) if err2 != nil { t.Fatalf("Expected second reminder to be sent, got %v", err2) } if !me.sentReminders[123][2] { t.Error("Expected email to be sent and recorded for quote 123, reminder 2") } if len(mq.reminders) != 2 { t.Errorf("Expected 2 reminders recorded in DB after both sends, got %d", len(mq.reminders)) } } // Test: Reminders for different quotes are independent // Description: Verifies that reminders for different quotes do not block each other and are recorded independently. Should fail if reminders for one quote affect another. func TestSendQuoteReminderEmail_DifferentQuotes(t *testing.T) { mq := &mockQueries{reminders: []db.QuoteReminder{}} me := &mockEmailService{} h := &QuotesHandler{queries: mq, emailService: me} // Send reminder for quote 123 err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil) if err1 != nil { t.Fatalf("Expected reminder for quote 123 to be sent, got %v", err1) } if !me.sentReminders[123][1] { t.Error("Expected email to be sent and recorded for quote 123, reminder 1") } if len(mq.reminders) != 1 { t.Errorf("Expected 1 reminder recorded in DB after first send, got %d", len(mq.reminders)) } // Send reminder for quote 456 err2 := h.SendQuoteReminderEmail(context.Background(), 456, 1, "test2@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 456, "ReminderType": 1}, nil) if err2 != nil { t.Fatalf("Expected reminder for quote 456 to be sent, got %v", err2) } if !me.sentReminders[456][1] { t.Error("Expected email to be sent and recorded for quote 456, reminder 1") } if len(mq.reminders) != 2 { t.Errorf("Expected 2 reminders recorded in DB after both sends, got %d", len(mq.reminders)) } }