Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -3,6 +3,7 @@ package mailer
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -145,6 +146,36 @@ func (g *GraphMailer) Send(ctx context.Context, msg Message) error {
|
||||
message["replyTo"] = replyToRecipients
|
||||
}
|
||||
|
||||
if len(msg.Attachments) > 0 {
|
||||
atts := make([]map[string]any, 0, len(msg.Attachments))
|
||||
for _, a := range msg.Attachments {
|
||||
if len(a.Data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(a.FileName)
|
||||
if name == "" {
|
||||
name = "attachment.bin"
|
||||
}
|
||||
|
||||
contentType := strings.TrimSpace(a.ContentType)
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
atts = append(atts, map[string]any{
|
||||
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||
"name": name,
|
||||
"contentType": contentType,
|
||||
"contentBytes": base64.StdEncoding.EncodeToString(a.Data),
|
||||
})
|
||||
}
|
||||
|
||||
if len(atts) > 0 {
|
||||
message["attachments"] = atts
|
||||
}
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"message": message,
|
||||
"saveToSentItems": true,
|
||||
|
||||
@@ -18,10 +18,17 @@ type Mailer struct {
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
To []string
|
||||
Subject string
|
||||
Body string
|
||||
BodyHTML string
|
||||
To []string
|
||||
Subject string
|
||||
Body string
|
||||
BodyHTML string
|
||||
Attachments []Attachment
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
FileName string
|
||||
ContentType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func New(cfg Config) *Mailer {
|
||||
|
||||
@@ -527,6 +527,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
{"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
|
||||
{"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)},
|
||||
{"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)},
|
||||
{"/api/order/send-market-mail", "POST", "update", routes.SendOrderMarketMailHandler(pgDB, mssql, ml)},
|
||||
{"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)},
|
||||
{"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},
|
||||
{"/api/min-price", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},
|
||||
|
||||
258
svc/routes/order_mail.go
Normal file
258
svc/routes/order_mail.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"bssapp-backend/internal/mailer"
|
||||
)
|
||||
|
||||
type sendOrderMarketMailPayload struct {
|
||||
OrderHeaderID string `json:"orderHeaderID"`
|
||||
}
|
||||
|
||||
func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
if ml == nil {
|
||||
http.Error(w, "mailer not initialized", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if pg == nil || mssql == nil {
|
||||
http.Error(w, "database not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var payload sendOrderMarketMailPayload
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orderID := strings.TrimSpace(payload.OrderHeaderID)
|
||||
if orderID == "" {
|
||||
http.Error(w, "orderHeaderID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, currAccCode, marketCode, marketTitle, err := resolveOrderMailContext(mssql, orderID)
|
||||
if err != nil {
|
||||
http.Error(w, "order context error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(marketCode) == "" && strings.TrimSpace(marketTitle) == "" {
|
||||
http.Error(w, "market not found for order/cari", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
recipients, err := loadMarketRecipients(pg, marketCode, marketTitle)
|
||||
if err != nil {
|
||||
http.Error(w, "recipient query error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(recipients) == 0 {
|
||||
http.Error(w, "no active email mapping for market", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, orderID)
|
||||
if err != nil {
|
||||
http.Error(w, "pdf build error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
number := strings.TrimSpace(orderNo)
|
||||
if number == "" && header != nil {
|
||||
number = strings.TrimSpace(header.OrderNumber)
|
||||
}
|
||||
if number == "" {
|
||||
number = orderID
|
||||
}
|
||||
|
||||
marketLabel := strings.TrimSpace(marketTitle)
|
||||
if marketLabel == "" {
|
||||
marketLabel = strings.TrimSpace(marketCode)
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("Sipariş %s - %s", number, marketLabel)
|
||||
bodyHTML := fmt.Sprintf(
|
||||
`<p>Sipariş kaydı oluşturuldu/güncellendi.</p><p><b>Sipariş No:</b> %s<br/><b>Cari:</b> %s<br/><b>Piyasa:</b> %s</p><p>PDF ektedir.</p>`,
|
||||
htmlEsc(number),
|
||||
htmlEsc(currAccCode),
|
||||
htmlEsc(marketLabel),
|
||||
)
|
||||
|
||||
fileNo := sanitizeFileName(number)
|
||||
if fileNo == "" {
|
||||
fileNo = orderID
|
||||
}
|
||||
|
||||
msg := mailer.Message{
|
||||
To: recipients,
|
||||
Subject: subject,
|
||||
BodyHTML: bodyHTML,
|
||||
Attachments: []mailer.Attachment{
|
||||
{
|
||||
FileName: "ORDER_" + fileNo + ".pdf",
|
||||
ContentType: "application/pdf",
|
||||
Data: pdfBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ml.Send(context.Background(), msg); err != nil {
|
||||
http.Error(w, "mail send error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"success": true,
|
||||
"orderHeaderID": orderID,
|
||||
"orderNumber": number,
|
||||
"marketCode": marketCode,
|
||||
"marketTitle": marketTitle,
|
||||
"recipients": recipients,
|
||||
"sentCount": len(recipients),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOrderMailContext(db *sql.DB, orderID string) (orderNo, currAccCode, marketCode, marketTitle string, err error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT TOP (1)
|
||||
ISNULL(h.OrderNumber, ''),
|
||||
ISNULL(h.CurrAccCode, ''),
|
||||
ISNULL(LTRIM(RTRIM(f.CustomerAtt01)), '') AS MarketCode,
|
||||
ISNULL(py.AttributeDescription, '') AS MarketTitle
|
||||
FROM BAGGI_V3.dbo.trOrderHeader h WITH (NOLOCK)
|
||||
LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter f WITH (NOLOCK)
|
||||
ON f.CurrAccCode = h.CurrAccCode
|
||||
LEFT JOIN BAGGI_V3.dbo.cdCurrAccAttributeDesc py WITH (NOLOCK)
|
||||
ON py.CurrAccTypeCode = h.CurrAccTypeCode
|
||||
AND py.AttributeTypeCode = 1
|
||||
AND py.AttributeCode = f.CustomerAtt01
|
||||
AND py.LangCode = 'TR'
|
||||
WHERE CAST(h.OrderHeaderID AS varchar(36)) = @p1
|
||||
`, orderID)
|
||||
|
||||
var no, cari, pCode, pTitle string
|
||||
if err = row.Scan(&no, &cari, &pCode, &pTitle); err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
|
||||
no = strings.TrimSpace(no)
|
||||
cari = strings.TrimSpace(cari)
|
||||
pCode = strings.TrimSpace(pCode)
|
||||
pTitle = strings.TrimSpace(pTitle)
|
||||
|
||||
return no, cari, pCode, pTitle, nil
|
||||
}
|
||||
|
||||
func loadMarketRecipients(pg *sql.DB, marketCode, marketTitle string) ([]string, error) {
|
||||
rows, err := pg.Query(`
|
||||
SELECT DISTINCT TRIM(m.email) AS email
|
||||
FROM mk_sales_piy p
|
||||
JOIN mk_market_mail mm
|
||||
ON mm.market_id = p.id
|
||||
JOIN mk_mail m
|
||||
ON m.id = mm.mail_id
|
||||
WHERE p.is_active = true
|
||||
AND m.is_active = true
|
||||
AND (
|
||||
UPPER(TRIM(p.code)) = UPPER(TRIM($1))
|
||||
OR ($2 <> '' AND UPPER(TRIM(p.title)) = UPPER(TRIM($2)))
|
||||
)
|
||||
AND COALESCE(TRIM(m.email), '') <> ''
|
||||
ORDER BY email
|
||||
`, strings.TrimSpace(marketCode), strings.TrimSpace(marketTitle))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]string, 0, 8)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildOrderPDFBytesForMail(db *sql.DB, orderID string) ([]byte, *OrderHeader, error) {
|
||||
header, err := getOrderHeaderFromDB(db, orderID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
lines, err := getOrderLinesFromDB(db, orderID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hasVat := false
|
||||
var vatRate float64
|
||||
for _, l := range lines {
|
||||
if l.VatRate.Valid && l.VatRate.Float64 > 0 {
|
||||
hasVat = true
|
||||
vatRate = l.VatRate.Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rows := normalizeOrderLinesForPdf(lines)
|
||||
|
||||
pdf, err := newOrderPdf()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
renderOrderGrid(pdf, header, rows, hasVat, vatRate)
|
||||
if err := pdf.Error(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), header, nil
|
||||
}
|
||||
|
||||
func sanitizeFileName(v string) string {
|
||||
s := strings.TrimSpace(v)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
invalid := []string{`\\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`}
|
||||
for _, bad := range invalid {
|
||||
s = strings.ReplaceAll(s, bad, "_")
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func htmlEsc(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
`"`, """,
|
||||
"'", "'",
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
Reference in New Issue
Block a user