301 lines
8.4 KiB
Go
301 lines
8.4 KiB
Go
package routes
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"bssapp-backend/db"
|
|
"bssapp-backend/internal/mailer"
|
|
"bssapp-backend/models"
|
|
"bssapp-backend/queries"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
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(rows []models.ProductPricing, actor string, at time.Time) string {
|
|
var b strings.Builder
|
|
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>`)
|
|
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 style="margin-top:10px;">Detaylar ekteki PDF dosyasindadir.</div>`)
|
|
b.WriteString(`</div>`)
|
|
b.WriteString(`</div>`)
|
|
return b.String()
|
|
}
|
|
|
|
func buildPricingChangeMailPDF(rows []models.ProductPricing, actor string, at time.Time) ([]byte, error) {
|
|
pdf := gofpdf.New("L", "mm", "A2", "")
|
|
font := "Arial"
|
|
if err := registerDejavuFonts(pdf, "dejavu"); err == nil {
|
|
font = "dejavu"
|
|
} else {
|
|
log.Printf("[pricing-mail] pdf font fallback: %v", err)
|
|
}
|
|
pdf.SetMargins(7, 8, 7)
|
|
pdf.SetAutoPageBreak(true, 10)
|
|
pdf.AddPage()
|
|
pdf.SetFont(font, "B", 13)
|
|
pdf.CellFormat(0, 7, "Fiyat Degisikligi", "", 1, "L", false, 0, "")
|
|
pdf.SetFont(font, "", 8)
|
|
if strings.TrimSpace(actor) != "" {
|
|
pdf.CellFormat(0, 5, "Islem Yapan: "+strings.TrimSpace(actor), "", 1, "L", false, 0, "")
|
|
}
|
|
pdf.CellFormat(0, 5, "Tarih: "+at.Format("02.01.2006 15:04"), "", 1, "L", false, 0, "")
|
|
pdf.CellFormat(0, 5, fmt.Sprintf("Urun Sayisi: %d", len(rows)), "", 1, "L", false, 0, "")
|
|
pdf.Ln(2)
|
|
|
|
heads := []string{
|
|
"MARKA GRUBU", "MARKA", "BRAND", "URUN KODU",
|
|
"STOK", "STOK GIRIS", "SON MALIYET", "SON FIYAT",
|
|
"ASKILI", "KATEGORI", "URUN ANA GRUBU", "URUN ALT GRUBU", "ICERIK", "KARISIM",
|
|
"MALIYET", "TABAN USD", "TABAN TRY",
|
|
"USD1", "USD2", "USD3", "USD4", "USD5", "USD6",
|
|
"EUR1", "EUR2", "EUR3", "EUR4", "EUR5", "EUR6",
|
|
"TRY1", "TRY2", "TRY3", "TRY4", "TRY5", "TRY6",
|
|
}
|
|
widths := []float64{
|
|
20, 18, 18, 28,
|
|
12, 18, 18, 18,
|
|
14, 18, 22, 22, 20, 20,
|
|
18, 18, 18,
|
|
13, 13, 13, 13, 13, 13,
|
|
13, 13, 13, 13, 13, 13,
|
|
13, 13, 13, 13, 13, 13,
|
|
}
|
|
aligns := make([]string, len(heads))
|
|
for i := range aligns {
|
|
aligns[i] = "L"
|
|
}
|
|
for _, idx := range []int{4, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34} {
|
|
aligns[idx] = "R"
|
|
}
|
|
writePDFTableHeader(pdf, font, heads, widths)
|
|
|
|
pdf.SetFont(font, "", 5.7)
|
|
for _, r := range rows {
|
|
if pdf.GetY() > 405 {
|
|
pdf.AddPage()
|
|
writePDFTableHeader(pdf, font, heads, widths)
|
|
pdf.SetFont(font, "", 5.7)
|
|
}
|
|
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.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),
|
|
}
|
|
writePDFTableRow(pdf, cells, widths, aligns, 4.4)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := pdf.Output(&buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// 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 | %d urun", now.Format("02.01.2006 15:04"), len(list))
|
|
html := buildPricingChangeMailHTML(list, actor, now)
|
|
attachments := []mailer.Attachment(nil)
|
|
if data, err := buildPricingChangeMailPDF(list, actor, now); err != nil {
|
|
log.Printf("[pricing-mail] pdf build failed group=%s err=%v", group, err)
|
|
} else {
|
|
attachments = append(attachments, mailer.Attachment{
|
|
FileName: fmt.Sprintf("fiyat-degisikligi-%s.pdf", now.Format("20060102-1504")),
|
|
ContentType: "application/pdf",
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
Attachments: attachments,
|
|
})
|
|
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))
|
|
}
|
|
}
|
|
}
|