package routes import ( "context" "database/sql" "fmt" "log" "sort" "strings" "time" "bssapp-backend/db" "bssapp-backend/internal/mailer" "bssapp-backend/models" "bssapp-backend/queries" ) func loadPricingRecipients(pg *sql.DB, firstGroupCode string) ([]string, error) { rows, err := pg.Query(` SELECT DISTINCT TRIM(m.email) AS email FROM mk_pricing_first_group_mail f JOIN mk_mail m ON m.id = f.mail_id WHERE m.is_active = true AND COALESCE(TRIM(m.email), '') <> '' AND UPPER(TRIM(f.urun_ilk_grubu)) = UPPER(TRIM($1)) ORDER BY email `, strings.TrimSpace(firstGroupCode)) if err != nil { return nil, err } defer rows.Close() out := make([]string, 0, 16) for rows.Next() { var email string if err := rows.Scan(&email); err != nil { return nil, err } email = strings.TrimSpace(email) if email != "" { out = append(out, email) } } return out, rows.Err() } func htmlEscapeMini(s string) string { // Minimal safe escaping for our templated cells. r := strings.NewReplacer( "&", "&", "<", "<", ">", ">", "\"", """, "'", "'", ) return r.Replace(s) } func fmtMoneyMail(v float64) string { return fmt.Sprintf("%.2f", v) } func fmtQtyMail(v float64) string { return fmt.Sprintf("%.2f", v) } func fmtDateTRFromISO(d string) string { d = strings.TrimSpace(d) if len(d) >= 10 { d = d[:10] } parts := strings.Split(d, "-") if len(parts) != 3 { if d == "" { return "-" } return d } y, m, day := parts[0], parts[1], parts[2] if y == "" || m == "" || day == "" { return d } return day + "." + m + "." + y } func buildPricingChangeMailHTML(firstGroupCode string, rows []models.ProductPricing, actor string, at time.Time) string { // Keep it simple: wide, scrollable table. var b strings.Builder // NOTE: Mail clients often render small fonts; keep this comfortably readable. // Use large inline sizes (some clients still downscale); keep everything inline for maximum compatibility. b.WriteString(`
`) b.WriteString(`
`) b.WriteString(`
Fiyat Degisikligi
`) b.WriteString(`
Urun Ilk Grubu: ` + htmlEscapeMini(firstGroupCode) + `
`) if strings.TrimSpace(actor) != "" { b.WriteString(`
Islem Yapan: ` + htmlEscapeMini(actor) + `
`) } b.WriteString(`
Tarih: ` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `
`) b.WriteString(`
Urun Sayisi: ` + fmt.Sprintf("%d", len(rows)) + `
`) b.WriteString(`
`) b.WriteString(`
`) b.WriteString(``) b.WriteString(``) heads := []string{ "MARKA GRUBU", "MARKA", "BRAND CODE", "URUN KODU", "STOK ADET", "STOK GIRIS", "SON MALIYET", "SON FIYAT", "ASKILI YAN", "KATEGORI", "URUN ILK GRUBU", "URUN ANA GRUBU", "URUN ALT GRUBU", "ICERIK", "KARISIM", "MALIYET FIYATI", "TABAN USD", "TABAN TRY", "USD1", "USD2", "USD3", "USD4", "USD5", "USD6", "EUR1", "EUR2", "EUR3", "EUR4", "EUR5", "EUR6", "TRY1", "TRY2", "TRY3", "TRY4", "TRY5", "TRY6", } for _, h := range heads { b.WriteString(``) } b.WriteString(``) for _, r := range rows { b.WriteString(``) cells := []string{ r.BrandGroupSec, r.Marka, r.BrandCode, r.ProductCode, fmtQtyMail(r.StockQty), fmtDateTRFromISO(r.StockEntryDate), fmtDateTRFromISO(r.LastCostingDate), fmtDateTRFromISO(r.LastPricingDate), r.AskiliYan, r.Kategori, r.UrunIlkGrubu, r.UrunAnaGrubu, r.UrunAltGrubu, r.Icerik, r.Karisim, fmtMoneyMail(r.CostPrice), fmtMoneyMail(r.BasePriceUsd), fmtMoneyMail(r.BasePriceTry), fmtMoneyMail(r.USD1), fmtMoneyMail(r.USD2), fmtMoneyMail(r.USD3), fmtMoneyMail(r.USD4), fmtMoneyMail(r.USD5), fmtMoneyMail(r.USD6), fmtMoneyMail(r.EUR1), fmtMoneyMail(r.EUR2), fmtMoneyMail(r.EUR3), fmtMoneyMail(r.EUR4), fmtMoneyMail(r.EUR5), fmtMoneyMail(r.EUR6), fmtMoneyMail(r.TRY1), fmtMoneyMail(r.TRY2), fmtMoneyMail(r.TRY3), fmtMoneyMail(r.TRY4), fmtMoneyMail(r.TRY5), fmtMoneyMail(r.TRY6), } for i, c := range cells { align := "left" // right align numeric-ish cells if i >= 4 { switch i { case 4, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35: align = "right" } } b.WriteString(``) } b.WriteString(``) } b.WriteString(`
` + htmlEscapeMini(h) + `
` + htmlEscapeMini(strings.TrimSpace(c)) + `
`) b.WriteString(`
Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.
`) b.WriteString(`
`) return b.String() } // sendPricingChangeMails sends one mail per UrunIlkGrubu (group) based on mk_pricing_first_group_mail mapping. // It is designed to be called post-commit in a goroutine. func sendPricingChangeMails(bg context.Context, ml *mailer.GraphMailer, productCodes []string, actor string) { if ml == nil { return } pg := db.PgDB if pg == nil { log.Printf("[pricing-mail] skipped: pg not ready") return } // Ensure mapping tables exist. if err := ensureFirstGroupMailMappingTables(pg); err != nil { log.Printf("[pricing-mail] mapping bootstrap error: %v", err) return } ctx, cancel := context.WithTimeout(bg, 90*time.Second) defer cancel() codes := make([]string, 0, len(productCodes)) seen := map[string]struct{}{} for _, c := range productCodes { c = strings.TrimSpace(c) if c == "" { continue } if _, ok := seen[c]; ok { continue } seen[c] = struct{}{} codes = append(codes, c) } if len(codes) == 0 { return } rows, err := queries.GetAllProductPricingRows(ctx, 500, queries.ProductPricingFilters{ProductCode: codes}, "productCode", false) if err != nil { log.Printf("[pricing-mail] pricing rows query error: %v", err) return } if len(rows) == 0 { return } byGroup := map[string][]models.ProductPricing{} for _, r := range rows { g := strings.TrimSpace(r.UrunIlkGrubu) if g == "" { g = "UNKNOWN" } byGroup[g] = append(byGroup[g], r) } now := time.Now() for group, list := range byGroup { // No mapping = skip. recipients, err := loadPricingRecipients(pg, group) if err != nil { log.Printf("[pricing-mail] recipient query error group=%s err=%v", group, err) continue } if len(recipients) == 0 { log.Printf("[pricing-mail] no recipients mapped group=%s", group) continue } sort.Slice(list, func(i, j int) bool { return strings.TrimSpace(list[i].ProductCode) < strings.TrimSpace(list[j].ProductCode) }) subject := fmt.Sprintf("Fiyat Degisikligi | %s | %s | %d urun", group, now.Format("02.01.2006 15:04"), len(list)) html := buildPricingChangeMailHTML(group, list, actor, now) // Retry 2 times with backoff. backoff := []time.Duration{800 * time.Millisecond, 2500 * time.Millisecond} var lastErr error for attempt := 0; attempt < len(backoff)+1; attempt++ { if attempt > 0 { time.Sleep(backoff[attempt-1]) } stepCtx, stepCancel := context.WithTimeout(bg, 25*time.Second) err := ml.Send(stepCtx, mailer.Message{ To: recipients, Subject: subject, BodyHTML: html, }) stepCancel() if err == nil { lastErr = nil break } lastErr = err } if lastErr != nil { log.Printf("[pricing-mail] send failed group=%s err=%v", group, lastErr) } else { log.Printf("[pricing-mail] sent group=%s to=%d products=%d", group, len(recipients), len(list)) } } }