266 lines
7.8 KiB
Go
266 lines
7.8 KiB
Go
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(`<div style="font-family:Segoe UI, Arial, sans-serif; font-size:18px; line-height:1.35; -webkit-text-size-adjust:100%;">`)
|
|
b.WriteString(`<div style="margin-bottom:10px;">`)
|
|
b.WriteString(`<div style="font-size:22px; margin-bottom:4px;"><b>Fiyat Degisikligi</b></div>`)
|
|
b.WriteString(`<div>Urun Ilk Grubu: <b>` + htmlEscapeMini(firstGroupCode) + `</b></div>`)
|
|
if strings.TrimSpace(actor) != "" {
|
|
b.WriteString(`<div>Islem Yapan: <b>` + htmlEscapeMini(actor) + `</b></div>`)
|
|
}
|
|
b.WriteString(`<div>Tarih: <b>` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `</b></div>`)
|
|
b.WriteString(`<div>Urun Sayisi: <b>` + fmt.Sprintf("%d", len(rows)) + `</b></div>`)
|
|
b.WriteString(`</div>`)
|
|
|
|
b.WriteString(`<div style="max-width:100%; overflow-x:auto;">`)
|
|
b.WriteString(`<table style="border-collapse:collapse; font-size:16px; white-space:nowrap;">`)
|
|
b.WriteString(`<thead><tr>`)
|
|
|
|
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(`<th style="border:1px solid #d0d0d0; background:#f3f3f3; padding:8px 10px; text-align:left; font-size:16px;">` + htmlEscapeMini(h) + `</th>`)
|
|
}
|
|
b.WriteString(`</tr></thead><tbody>`)
|
|
|
|
for _, r := range rows {
|
|
b.WriteString(`<tr>`)
|
|
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(`<td style="border:1px solid #e0e0e0; padding:8px 10px; text-align:` + align + `;">` + htmlEscapeMini(strings.TrimSpace(c)) + `</td>`)
|
|
}
|
|
b.WriteString(`</tr>`)
|
|
}
|
|
|
|
b.WriteString(`</tbody></table></div>`)
|
|
b.WriteString(`<div style="margin-top:12px; font-size:14px; color:#666;">Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.</div>`)
|
|
b.WriteString(`</div>`)
|
|
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))
|
|
}
|
|
}
|
|
}
|