Merge remote-tracking branch 'origin/master'
This commit is contained in:
265
svc/routes/product_pricing_change_mail.go
Normal file
265
svc/routes/product_pricing_change_mail.go
Normal file
@@ -0,0 +1,265 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user