Files
bssapp/svc/routes/wholesale_campaign_mail.go
2026-06-18 18:33:48 +03:00

386 lines
10 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"
"github.com/lib/pq"
)
type wholesaleCampaignMailRow struct {
ProductCode string
UrunIlkGrubu string
Marka string
BrandGroupSec string
Dim1 int64
Dim3 int64
Dim1Token string
Dim3Token string
CampaignCode string
CampaignTitle string
DiscountRate float64
}
func buildWholesaleCampaignChangeMailHTML(rows []wholesaleCampaignMailRow, 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>Kampanya 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>Varyant 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 buildWholesaleCampaignChangeMailPDF(rows []wholesaleCampaignMailRow, actor string, at time.Time) ([]byte, error) {
pdf := gofpdf.New("L", "mm", "A4", "")
font := "Arial"
if err := registerDejavuFonts(pdf, "dejavu"); err == nil {
font = "dejavu"
} else {
log.Printf("[campaign-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, "Kampanya 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("Varyant Sayisi: %d", len(rows)), "", 1, "L", false, 0, "")
pdf.Ln(2)
heads := []string{
"MARKA GRUBU",
"MARKA",
"URUN KODU",
"DIM1",
"DIM3",
"KAMPANYA",
"IND %",
}
widths := []float64{35, 26, 35, 20, 20, 118, 16}
aligns := []string{"L", "L", "L", "R", "R", "L", "R"}
writePDFTableHeader(pdf, font, heads, widths)
pdf.SetFont(font, "", 7)
for _, r := range rows {
if pdf.GetY() > 190 {
pdf.AddPage()
writePDFTableHeader(pdf, font, heads, widths)
pdf.SetFont(font, "", 7)
}
campaignLabel := strings.TrimSpace(r.CampaignCode)
if t := strings.TrimSpace(r.CampaignTitle); t != "" {
if campaignLabel != "" {
campaignLabel = campaignLabel + " - " + t
} else {
campaignLabel = t
}
}
cells := []string{
r.BrandGroupSec,
r.Marka,
r.ProductCode,
strings.TrimSpace(r.Dim1Token),
func() string {
if strings.TrimSpace(r.Dim3Token) != "" {
return strings.TrimSpace(r.Dim3Token)
}
return ""
}(),
campaignLabel,
fmt.Sprintf("%.2f", r.DiscountRate),
}
writePDFTableRow(pdf, cells, widths, aligns, 5)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// sendWholesaleCampaignChangeMails sends one mail per UrunIlkGrubu using existing pricing mail mapping tables.
// It lists only variants that currently have a campaign assigned.
func sendWholesaleCampaignChangeMails(bg context.Context, ml *mailer.GraphMailer, productCodes []string, actor string) {
if ml == nil {
return
}
pg := db.PgDB
if pg == nil {
log.Printf("[campaign-mail] skipped: pg not ready")
return
}
// Ensure mapping tables exist (reuse pricing mapping).
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
log.Printf("[campaign-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
}
// Product info for grouping (UrunIlkGrubu, Marka, BrandGroupSec) comes from Nebim query.
// This is best-effort: if MSSQL is down, we still send a single mail under group "UNKNOWN".
productInfo := map[string]models.ProductPricing{}
{
rows, err := queries.GetAllProductPricingRows(ctx, 500, queries.ProductPricingFilters{ProductCode: codes}, "productCode", false)
if err == nil {
for _, r := range rows {
code := strings.TrimSpace(r.ProductCode)
if code != "" {
productInfo[code] = r
}
}
}
}
type dbRow struct {
ProductCode string
Dim1 int64
Dim3 sql.NullInt64
CampaignCode string
CampaignTitle string
DiscountRate float64
}
rows, err := pg.QueryContext(ctx, `
WITH mm AS (
SELECT id AS mmitem_id, code
FROM mmitem
WHERE code = ANY($1::text[])
),
latest AS (
SELECT DISTINCT ON (z.mmitem_id, z.dim1, COALESCE(z.dim3, 0))
z.mmitem_id,
z.dim1,
z.dim3,
z.sdcampaign_id
FROM zbggcampaign z
JOIN mm ON mm.mmitem_id = z.mmitem_id
ORDER BY z.mmitem_id, z.dim1, COALESCE(z.dim3, 0), z.id DESC
)
SELECT
mm.code AS product_code,
l.dim1,
l.dim3,
COALESCE(sc.code,'') AS campaign_code,
COALESCE(sc.title,'') AS campaign_title,
COALESCE(sc.discount_rate, 0)::float8 AS discount_rate
FROM mm
JOIN latest l
ON l.mmitem_id = mm.mmitem_id
JOIN sdcampaign sc
ON sc.id = l.sdcampaign_id
WHERE COALESCE(sc.is_active, TRUE) = TRUE
ORDER BY mm.code, l.dim1, COALESCE(l.dim3, 0), sc.discount_rate DESC;
`, pq.Array(codes))
if err != nil {
log.Printf("[campaign-mail] campaign rows query error: %v", err)
return
}
defer rows.Close()
// Resolve dim ids -> human tokens so mails show Nebim-style variant codes (not numeric ids).
resolveDimTokens := func(ctx context.Context, dimIDs []int64) map[int64]string {
out := make(map[int64]string, len(dimIDs))
uniq := make([]int64, 0, len(dimIDs))
seen := make(map[int64]struct{}, len(dimIDs))
for _, id := range dimIDs {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
uniq = append(uniq, id)
}
if len(uniq) == 0 {
return out
}
tRows, err := pg.QueryContext(ctx, `
SELECT DISTINCT ON (dim_id)
dim_id,
token
FROM mk_dim_token_map
WHERE dim_column = 'dimval1'
AND dim_id = ANY($1::bigint[])
ORDER BY dim_id, updated_at DESC;
`, pq.Array(uniq))
if err != nil {
return out
}
defer tRows.Close()
for tRows.Next() {
var id int64
var tok string
if err := tRows.Scan(&id, &tok); err == nil {
tok = strings.TrimSpace(tok)
if tok != "" {
out[id] = tok
}
}
}
return out
}
mailRows := make([]wholesaleCampaignMailRow, 0, 1024)
dimIDs := make([]int64, 0, 2048)
for rows.Next() {
var r dbRow
if err := rows.Scan(&r.ProductCode, &r.Dim1, &r.Dim3, &r.CampaignCode, &r.CampaignTitle, &r.DiscountRate); err != nil {
log.Printf("[campaign-mail] scan error: %v", err)
return
}
code := strings.TrimSpace(r.ProductCode)
info := productInfo[code]
group := strings.TrimSpace(info.UrunIlkGrubu)
if group == "" {
group = "UNKNOWN"
}
d3 := int64(0)
if r.Dim3.Valid {
d3 = r.Dim3.Int64
}
if r.Dim1 > 0 {
dimIDs = append(dimIDs, r.Dim1)
}
if d3 > 0 {
dimIDs = append(dimIDs, d3)
}
mailRows = append(mailRows, wholesaleCampaignMailRow{
ProductCode: code,
UrunIlkGrubu: group,
Marka: strings.TrimSpace(info.Marka),
BrandGroupSec: strings.TrimSpace(info.BrandGroupSec),
Dim1: r.Dim1,
Dim3: d3,
CampaignCode: strings.TrimSpace(r.CampaignCode),
CampaignTitle: strings.TrimSpace(r.CampaignTitle),
DiscountRate: r.DiscountRate,
})
}
if err := rows.Err(); err != nil {
log.Printf("[campaign-mail] rows error: %v", err)
return
}
if len(mailRows) == 0 {
// Nothing assigned => no mail.
return
}
tokens := resolveDimTokens(ctx, dimIDs)
for i := range mailRows {
mailRows[i].Dim1Token = tokens[mailRows[i].Dim1]
if mailRows[i].Dim1Token == "" && mailRows[i].Dim1 > 0 {
mailRows[i].Dim1Token = fmt.Sprintf("%d", mailRows[i].Dim1)
}
if mailRows[i].Dim3 > 0 {
mailRows[i].Dim3Token = tokens[mailRows[i].Dim3]
if mailRows[i].Dim3Token == "" {
mailRows[i].Dim3Token = fmt.Sprintf("%d", mailRows[i].Dim3)
}
}
}
byGroup := map[string][]wholesaleCampaignMailRow{}
for _, r := range mailRows {
g := strings.TrimSpace(r.UrunIlkGrubu)
if g == "" {
g = "UNKNOWN"
}
byGroup[g] = append(byGroup[g], r)
}
now := time.Now()
for group, list := range byGroup {
recipients, err := loadPricingRecipients(pg, group)
if err != nil {
log.Printf("[campaign-mail] recipient query error group=%s err=%v", group, err)
continue
}
if len(recipients) == 0 {
log.Printf("[campaign-mail] no recipients mapped group=%s", group)
continue
}
sort.Slice(list, func(i, j int) bool {
if list[i].ProductCode != list[j].ProductCode {
return list[i].ProductCode < list[j].ProductCode
}
if list[i].Dim1 != list[j].Dim1 {
return list[i].Dim1 < list[j].Dim1
}
return list[i].Dim3 < list[j].Dim3
})
subject := fmt.Sprintf("Kampanya Degisikligi | %s | %d varyant", now.Format("02.01.2006 15:04"), len(list))
html := buildWholesaleCampaignChangeMailHTML(list, actor, now)
attachments := []mailer.Attachment(nil)
if data, err := buildWholesaleCampaignChangeMailPDF(list, actor, now); err != nil {
log.Printf("[campaign-mail] pdf build failed group=%s err=%v", group, err)
} else {
attachments = append(attachments, mailer.Attachment{
FileName: fmt.Sprintf("kampanya-degisikligi-%s.pdf", now.Format("20060102-1504")),
ContentType: "application/pdf",
Data: data,
})
}
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 {
log.Printf("[campaign-mail] send failed group=%s err=%v", group, err)
} else {
log.Printf("[campaign-mail] sent group=%s to=%d variants=%d", group, len(recipients), len(list))
}
}
}