Adding manual email templates, adding support for pdf attachments in emails, attaching quote to reminder if possible
This commit is contained in:
parent
8dbf221908
commit
d898149810
|
|
@ -3,14 +3,22 @@ package email
|
|||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Attachment represents an email attachment
|
||||
type Attachment struct {
|
||||
Filename string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
var (
|
||||
emailServiceInstance *EmailService
|
||||
once sync.Once
|
||||
|
|
@ -50,6 +58,20 @@ func GetEmailService() *EmailService {
|
|||
|
||||
// SendTemplateEmail renders a template and sends an email with optional CC and BCC.
|
||||
func (es *EmailService) SendTemplateEmail(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string) error {
|
||||
return es.SendTemplateEmailWithAttachments(to, subject, templateName, data, ccs, bccs, nil)
|
||||
}
|
||||
|
||||
// SendTemplateEmailWithAttachments renders a template and sends an email with optional CC, BCC, and attachments.
|
||||
func (es *EmailService) SendTemplateEmailWithAttachments(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string, attachments []interface{}) error {
|
||||
// Convert interface{} attachments to []Attachment
|
||||
var typedAttachments []Attachment
|
||||
for _, att := range attachments {
|
||||
if a, ok := att.(Attachment); ok {
|
||||
typedAttachments = append(typedAttachments, a)
|
||||
} else if a, ok := att.(struct{ Filename, FilePath string }); ok {
|
||||
typedAttachments = append(typedAttachments, Attachment{Filename: a.Filename, FilePath: a.FilePath})
|
||||
}
|
||||
}
|
||||
defaultBccs := []string{"carpis@cmctechnologies.com.au"}
|
||||
bccs = append(defaultBccs, bccs...)
|
||||
|
||||
|
|
@ -65,22 +87,80 @@ func (es *EmailService) SendTemplateEmail(to string, subject string, templateNam
|
|||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = es.FromAddress
|
||||
headers["To"] = to
|
||||
if len(ccs) > 0 {
|
||||
headers["Cc"] = joinAddresses(ccs)
|
||||
}
|
||||
headers["Subject"] = subject
|
||||
headers["MIME-Version"] = "1.0"
|
||||
headers["Content-Type"] = "text/html; charset=\"UTF-8\""
|
||||
|
||||
var msg bytes.Buffer
|
||||
for k, v := range headers {
|
||||
fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
|
||||
|
||||
// If there are attachments, use multipart message
|
||||
if len(typedAttachments) > 0 {
|
||||
boundary := "boundary123456789"
|
||||
|
||||
// Write headers
|
||||
fmt.Fprintf(&msg, "From: %s\r\n", es.FromAddress)
|
||||
fmt.Fprintf(&msg, "To: %s\r\n", to)
|
||||
if len(ccs) > 0 {
|
||||
fmt.Fprintf(&msg, "Cc: %s\r\n", joinAddresses(ccs))
|
||||
}
|
||||
fmt.Fprintf(&msg, "Subject: %s\r\n", subject)
|
||||
fmt.Fprintf(&msg, "MIME-Version: 1.0\r\n")
|
||||
fmt.Fprintf(&msg, "Content-Type: multipart/mixed; boundary=%s\r\n", boundary)
|
||||
msg.WriteString("\r\n")
|
||||
|
||||
// Write HTML body part
|
||||
fmt.Fprintf(&msg, "--%s\r\n", boundary)
|
||||
msg.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n")
|
||||
msg.WriteString("\r\n")
|
||||
msg.Write(body.Bytes())
|
||||
msg.WriteString("\r\n")
|
||||
|
||||
// Write attachments
|
||||
for _, att := range typedAttachments {
|
||||
file, err := os.Open(att.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open attachment %s: %w", att.FilePath, err)
|
||||
}
|
||||
|
||||
fileData, err := io.ReadAll(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read attachment %s: %w", att.FilePath, err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&msg, "--%s\r\n", boundary)
|
||||
fmt.Fprintf(&msg, "Content-Type: application/pdf\r\n")
|
||||
fmt.Fprintf(&msg, "Content-Transfer-Encoding: base64\r\n")
|
||||
fmt.Fprintf(&msg, "Content-Disposition: attachment; filename=\"%s\"\r\n", att.Filename)
|
||||
msg.WriteString("\r\n")
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(fileData)
|
||||
// Split into lines of 76 characters for proper MIME formatting
|
||||
for i := 0; i < len(encoded); i += 76 {
|
||||
end := i + 76
|
||||
if end > len(encoded) {
|
||||
end = len(encoded)
|
||||
}
|
||||
msg.WriteString(encoded[i:end])
|
||||
msg.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(&msg, "--%s--\r\n", boundary)
|
||||
} else {
|
||||
// Simple message without attachments
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = es.FromAddress
|
||||
headers["To"] = to
|
||||
if len(ccs) > 0 {
|
||||
headers["Cc"] = joinAddresses(ccs)
|
||||
}
|
||||
headers["Subject"] = subject
|
||||
headers["MIME-Version"] = "1.0"
|
||||
headers["Content-Type"] = "text/html; charset=\"UTF-8\""
|
||||
|
||||
for k, v := range headers {
|
||||
fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
|
||||
}
|
||||
msg.WriteString("\r\n")
|
||||
msg.Write(body.Bytes())
|
||||
}
|
||||
msg.WriteString("\r\n")
|
||||
msg.Write(body.Bytes())
|
||||
|
||||
recipients := []string{to}
|
||||
recipients = append(recipients, ccs...)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -181,6 +182,7 @@ type QuoteQueries interface {
|
|||
|
||||
type EmailSender interface {
|
||||
SendTemplateEmail(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string) error
|
||||
SendTemplateEmailWithAttachments(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string, attachments []interface{}) error
|
||||
}
|
||||
|
||||
type QuotesHandler struct {
|
||||
|
|
@ -317,6 +319,20 @@ func getReminderDetails(reminderType QuoteReminderType) (subject, template strin
|
|||
}
|
||||
}
|
||||
|
||||
// getReminderDetailsManual returns the subject and template for a manually sent reminder (generic templates without time references)
|
||||
func getReminderDetailsManual(reminderType QuoteReminderType) (subject, template string) {
|
||||
switch reminderType {
|
||||
case FirstReminder:
|
||||
return "Reminder: Quote %s Expires Soon", "manual_first_reminder.html"
|
||||
case SecondReminder:
|
||||
return "Follow-Up: Quote %s Expired", "manual_second_reminder.html"
|
||||
case ThirdReminder:
|
||||
return "Final Reminder: Quote %s Closed", "manual_final_reminder.html"
|
||||
default:
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
||||
// formatQuoteDates formats ISO date strings to DD/MM/YYYY format
|
||||
func formatQuoteDates(dateIssuedStr, validUntilStr string) (submissionDate, expiryDate string) {
|
||||
if dateIssuedStr != "" {
|
||||
|
|
@ -398,6 +414,7 @@ func (h *QuotesHandler) DailyQuoteExpirationCheck() {
|
|||
templateData,
|
||||
ccs,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error sending %s for quote %v: %v\n", job.ReminderType.String(), q["ID"], err)
|
||||
|
|
@ -409,7 +426,7 @@ func (h *QuotesHandler) DailyQuoteExpirationCheck() {
|
|||
}
|
||||
|
||||
// SendQuoteReminderEmail checks if a reminder of the given type has already been sent for the quote, sends the email if not, and records it.
|
||||
func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string) error {
|
||||
func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string, allowDuplicate bool) error {
|
||||
// Safeguard: check for valid recipient
|
||||
if strings.TrimSpace(recipient) == "" {
|
||||
return fmt.Errorf("recipient email is required")
|
||||
|
|
@ -423,22 +440,24 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3
|
|||
return fmt.Errorf("invalid reminder type: %v", reminderType)
|
||||
}
|
||||
|
||||
// Check if reminder already sent
|
||||
reminders, err := h.queries.GetQuoteRemindersByType(ctx, db.GetQuoteRemindersByTypeParams{
|
||||
QuoteID: quoteID,
|
||||
ReminderType: int32(reminderType),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing reminders: %w", err)
|
||||
}
|
||||
// Check if reminder already sent (only if duplicates not allowed)
|
||||
if !allowDuplicate {
|
||||
reminders, err := h.queries.GetQuoteRemindersByType(ctx, db.GetQuoteRemindersByTypeParams{
|
||||
QuoteID: quoteID,
|
||||
ReminderType: int32(reminderType),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing reminders: %w", err)
|
||||
}
|
||||
|
||||
// Exit if the email has already been sent
|
||||
if len(reminders) > 0 {
|
||||
return nil
|
||||
// Exit if the email has already been sent
|
||||
if len(reminders) > 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Send the email
|
||||
err = h.emailService.SendTemplateEmail(
|
||||
err := h.emailService.SendTemplateEmail(
|
||||
recipient,
|
||||
subject,
|
||||
templateName,
|
||||
|
|
@ -468,6 +487,71 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3
|
|||
return nil
|
||||
}
|
||||
|
||||
// SendQuoteReminderEmailWithAttachment is like SendQuoteReminderEmail but includes a PDF attachment
|
||||
func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string, pdfPath string, pdfFilename string) error {
|
||||
// Safeguard: check for valid recipient
|
||||
if strings.TrimSpace(recipient) == "" {
|
||||
return fmt.Errorf("recipient email is required")
|
||||
}
|
||||
// Safeguard: check for valid template data
|
||||
if templateData == nil {
|
||||
return fmt.Errorf("template data is required")
|
||||
}
|
||||
// Safeguard: check for valid reminder type
|
||||
if reminderType != FirstReminder && reminderType != SecondReminder && reminderType != ThirdReminder {
|
||||
return fmt.Errorf("invalid reminder type: %v", reminderType)
|
||||
}
|
||||
|
||||
// Check if PDF exists and prepare attachment if found
|
||||
var attachments []interface{}
|
||||
if _, err := os.Stat(pdfPath); err == nil {
|
||||
// PDF exists, attach it
|
||||
type Attachment struct {
|
||||
Filename string
|
||||
FilePath string
|
||||
}
|
||||
attachments = []interface{}{
|
||||
Attachment{
|
||||
Filename: pdfFilename,
|
||||
FilePath: pdfPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
// If PDF doesn't exist, just send email without attachment
|
||||
|
||||
// Send the email (with or without attachment)
|
||||
err := h.emailService.SendTemplateEmailWithAttachments(
|
||||
recipient,
|
||||
subject,
|
||||
templateName,
|
||||
templateData,
|
||||
ccs,
|
||||
nil,
|
||||
attachments,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send email: %w", err)
|
||||
}
|
||||
|
||||
// Record the reminder
|
||||
var user sql.NullString
|
||||
if username != nil {
|
||||
user = sql.NullString{String: *username, Valid: true}
|
||||
} else {
|
||||
user = sql.NullString{Valid: false}
|
||||
}
|
||||
_, err = h.queries.InsertQuoteReminder(ctx, db.InsertQuoteReminderParams{
|
||||
QuoteID: quoteID,
|
||||
ReminderType: int32(reminderType),
|
||||
DateSent: time.Now().UTC(),
|
||||
Username: user,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to record reminder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendManualReminder handles POST requests to manually send a quote reminder
|
||||
func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
|
|
@ -515,22 +599,34 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
// Use shared function to prepare email data
|
||||
subject, template, templateData, ccs := prepareReminderEmail(
|
||||
reminderType,
|
||||
customerName,
|
||||
dateIssuedStr,
|
||||
validUntilStr,
|
||||
enquiryRef,
|
||||
userEmail,
|
||||
)
|
||||
// Use manual template for manually sent reminders (generic without time references)
|
||||
subject, template := getReminderDetailsManual(reminderType)
|
||||
subject = fmt.Sprintf(subject, enquiryRef)
|
||||
|
||||
submissionDate, expiryDate := formatQuoteDates(dateIssuedStr, validUntilStr)
|
||||
|
||||
templateData := map[string]interface{}{
|
||||
"CustomerName": customerName,
|
||||
"SubmissionDate": submissionDate,
|
||||
"ExpiryDate": expiryDate,
|
||||
"QuoteRef": enquiryRef,
|
||||
}
|
||||
|
||||
ccs := []string{"sales@cmctechnologies.com.au"}
|
||||
if userEmail != "" {
|
||||
ccs = append(ccs, userEmail)
|
||||
}
|
||||
|
||||
// Attach PDF quote
|
||||
pdfPath := fmt.Sprintf("webroot/pdf/%d.pdf", quoteID)
|
||||
pdfFilename := fmt.Sprintf("%s.pdf", enquiryRef)
|
||||
|
||||
// Get username from request
|
||||
username := getUsername(r)
|
||||
usernamePtr := &username
|
||||
|
||||
// Send the reminder
|
||||
err = h.SendQuoteReminderEmail(
|
||||
// Send the reminder with attachment
|
||||
err = h.SendQuoteReminderEmailWithAttachment(
|
||||
r.Context(),
|
||||
int32(quoteID),
|
||||
reminderType,
|
||||
|
|
@ -540,6 +636,8 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques
|
|||
templateData,
|
||||
ccs,
|
||||
usernamePtr,
|
||||
pdfPath,
|
||||
pdfFilename,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func TestSendQuoteReminderEmail_OnDesignatedDay(t *testing.T) {
|
|||
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, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
|
@ -164,7 +164,7 @@ func TestSendQuoteReminderEmail_AlreadyReminded(t *testing.T) {
|
|||
queries: mq,
|
||||
emailService: me,
|
||||
}
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error for already sent reminder")
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) {
|
|||
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, nil)
|
||||
err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil, false)
|
||||
if err1 != nil {
|
||||
t.Fatalf("Expected first call to succeed, got %v", err1)
|
||||
}
|
||||
|
|
@ -197,7 +197,7 @@ func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) {
|
|||
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, nil)
|
||||
err2 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil, false)
|
||||
if err2 == nil {
|
||||
t.Error("Expected error for already sent reminder on second call")
|
||||
}
|
||||
|
|
@ -215,7 +215,7 @@ func TestSendQuoteReminderEmail_SendsIfNotAlreadySent(t *testing.T) {
|
|||
queries: mq,
|
||||
emailService: me,
|
||||
}
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
|
@ -236,7 +236,7 @@ func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) {
|
|||
queries: mq,
|
||||
emailService: me,
|
||||
}
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error for already sent reminder")
|
||||
}
|
||||
|
|
@ -252,7 +252,7 @@ func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) {
|
|||
// 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, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error when DB fails, got nil")
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ func TestSendQuoteReminderEmail_DBError(t *testing.T) {
|
|||
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, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "", "Subject", "template", map[string]interface{}{}, nil, nil, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil recipient, got nil")
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@ func TestSendQuoteReminderEmail_NilRecipient(t *testing.T) {
|
|||
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, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", nil, nil, nil, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing template data, got nil")
|
||||
}
|
||||
|
|
@ -288,7 +288,7 @@ func TestSendQuoteReminderEmail_MissingTemplateData(t *testing.T) {
|
|||
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, nil)
|
||||
err := h.SendQuoteReminderEmail(context.Background(), 123, 99, "test@example.com", "Subject", "template", map[string]interface{}{}, nil, nil, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid reminder type, got nil")
|
||||
}
|
||||
|
|
@ -302,7 +302,7 @@ func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) {
|
|||
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, nil)
|
||||
err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil, false)
|
||||
if err1 != nil {
|
||||
t.Fatalf("Expected first reminder to be sent, got %v", err1)
|
||||
}
|
||||
|
|
@ -314,7 +314,7 @@ func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) {
|
|||
}
|
||||
|
||||
// Second reminder type
|
||||
err2 := h.SendQuoteReminderEmail(context.Background(), 123, 2, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 2}, nil, nil)
|
||||
err2 := h.SendQuoteReminderEmail(context.Background(), 123, 2, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 2}, nil, nil, false)
|
||||
if err2 != nil {
|
||||
t.Fatalf("Expected second reminder to be sent, got %v", err2)
|
||||
}
|
||||
|
|
@ -334,7 +334,7 @@ func TestSendQuoteReminderEmail_DifferentQuotes(t *testing.T) {
|
|||
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, nil)
|
||||
err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil, nil, false)
|
||||
if err1 != nil {
|
||||
t.Fatalf("Expected reminder for quote 123 to be sent, got %v", err1)
|
||||
}
|
||||
|
|
@ -346,7 +346,7 @@ func TestSendQuoteReminderEmail_DifferentQuotes(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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, nil)
|
||||
err2 := h.SendQuoteReminderEmail(context.Background(), 456, 1, "test2@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 456, "ReminderType": 1}, nil, nil, false)
|
||||
if err2 != nil {
|
||||
t.Fatalf("Expected reminder for quote 456 to be sent, got %v", err2)
|
||||
}
|
||||
|
|
|
|||
12
go/templates/quotes/manual_final_reminder.html
Normal file
12
go/templates/quotes/manual_final_reminder.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<p>Dear {{.CustomerName}},</p>
|
||||
|
||||
<p>We're getting in touch regarding the quotation issued on {{.SubmissionDate}}, reference <strong>{{.QuoteRef}}</strong>, expiring {{.ExpiryDate}}. As we haven't received a response, we wanted to reach out one final time.</p>
|
||||
|
||||
<p>This quotation is no longer valid, and we'll consider the proposal closed unless we hear otherwise. If you're still interested in moving forward, we'd be happy to provide a new quote tailored to your current requirements.</p>
|
||||
|
||||
<p>If you have any outstanding questions or would like to initiate a new quote, simply reply to this email — we're here to help.</p>
|
||||
|
||||
<p>Thank you again for your time and consideration.</p>
|
||||
|
||||
<p>Warm regards,</p>
|
||||
<p>CMC Technologies</p>
|
||||
15
go/templates/quotes/manual_first_reminder.html
Normal file
15
go/templates/quotes/manual_first_reminder.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<html>
|
||||
<body>
|
||||
<p>Dear {{.CustomerName}},</p>
|
||||
|
||||
<p>We'd like to remind you that the quote we provided on {{.SubmissionDate}}, reference <strong>{{.QuoteRef}}</strong>, will expire on {{.ExpiryDate}}.</p>
|
||||
|
||||
<p>If you require more time, we're happy to help — simply reply to this email to request an extension or a refreshed version of the quote.</p>
|
||||
|
||||
<p>Should you have any questions or need further clarification, please don't hesitate to get in touch. We're here to support you and ensure everything is in order.</p>
|
||||
|
||||
<p>Thank you for time and consideration. We look forward to the opportunity to work with you.</p>
|
||||
<p>Warm regards,</p>
|
||||
<p>CMC Technologies</p>
|
||||
</body>
|
||||
</html>
|
||||
12
go/templates/quotes/manual_second_reminder.html
Normal file
12
go/templates/quotes/manual_second_reminder.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<p>Dear {{.CustomerName}},</p>
|
||||
|
||||
<p>We're following up on the quotation submitted on {{.SubmissionDate}}, reference <strong>{{.QuoteRef}}</strong>, expiring {{.ExpiryDate}}. As we haven't heard back, we wanted to check whether you're still considering this proposal or if your requirements have changed.</p>
|
||||
|
||||
<p>If you'd like an updated or revised quote tailored to your current needs, simply reply to this email — we're more than happy to assist.</p>
|
||||
|
||||
<p>If you have any questions or feedback regarding the original quotation, please don't hesitate to let us know.</p>
|
||||
|
||||
<p>Thank you again for your time and consideration. We'd be glad to support you whenever you're ready.</p>
|
||||
|
||||
<p>Warm regards,</p>
|
||||
<p>CMC Technologies</p>
|
||||
Loading…
Reference in a new issue