Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 12:28:05 +03:00
parent e14c1c176a
commit a3d143ad70
14 changed files with 5123 additions and 21 deletions

View File

@@ -0,0 +1,286 @@
package routes
import (
"context"
"database/sql"
"fmt"
"log"
"sort"
"strings"
"time"
"bssapp-backend/db"
"bssapp-backend/internal/mailer"
"bssapp-backend/models"
"bssapp-backend/queries"
"github.com/lib/pq"
)
type wholesaleCampaignMailRow struct {
ProductCode string
UrunIlkGrubu string
Marka string
BrandGroupSec string
Dim1 int64
Dim3 int64
CampaignCode string
CampaignTitle string
DiscountRate float64
}
func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, 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>`)
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>Varyant 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",
"URUN KODU",
"DIM1",
"DIM3",
"KAMPANYA",
"IND %",
}
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>`)
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,
fmt.Sprintf("%d", r.Dim1),
func() string {
if r.Dim3 > 0 {
return fmt.Sprintf("%d", r.Dim3)
}
return ""
}(),
campaignLabel,
fmt.Sprintf("%.2f", r.DiscountRate),
}
for i, c := range cells {
align := "left"
if i == 3 || i == 4 || i == 6 {
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()
}
// 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()
mailRows := make([]wholesaleCampaignMailRow, 0, 1024)
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
}
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
}
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 | %s | %d varyant", group, now.Format("02.01.2006 15:04"), len(list))
html := buildWholesaleCampaignChangeMailHTML(group, list, actor, now)
stepCtx, stepCancel := context.WithTimeout(bg, 25*time.Second)
err = ml.Send(stepCtx, mailer.Message{
To: recipients,
Subject: subject,
BodyHTML: html,
})
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))
}
}
}