753 lines
21 KiB
Go
753 lines
21 KiB
Go
package routes
|
|
|
|
import (
|
|
"bssapp-backend/auth"
|
|
"bssapp-backend/models"
|
|
"bssapp-backend/queries"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// GET /api/pricing/products
|
|
func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
|
started := time.Now()
|
|
traceID := buildPricingTraceID(r)
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
|
if !ok || claims == nil {
|
|
log.Printf("[ProductPricing] trace=%s unauthorized method=%s path=%s", traceID, r.Method, r.URL.Path)
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
log.Printf("[ProductPricing] trace=%s start user=%s id=%d", traceID, claims.Username, claims.ID)
|
|
|
|
// Cloudflare upstream timeout is lower than 180s; fail fast and return API 504 instead of CDN 524.
|
|
ctx, cancel := context.WithTimeout(r.Context(), 110*time.Second)
|
|
defer cancel()
|
|
|
|
limit := 500
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 500 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
page := 1
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("page")); raw != "" {
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
|
page = parsed
|
|
}
|
|
}
|
|
filters := queries.ProductPricingFilters{
|
|
Search: strings.TrimSpace(r.URL.Query().Get("q")),
|
|
ProductCode: splitCSVParam(r.URL.Query().Get("product_code")),
|
|
BrandGroup: splitCSVParam(r.URL.Query().Get("brand_group_selection")),
|
|
AskiliYan: splitCSVParam(r.URL.Query().Get("askili_yan")),
|
|
Kategori: splitCSVParam(r.URL.Query().Get("kategori")),
|
|
UrunIlkGrubu: splitCSVParam(r.URL.Query().Get("urun_ilk_grubu")),
|
|
UrunAnaGrubu: splitCSVParam(r.URL.Query().Get("urun_ana_grubu")),
|
|
UrunAltGrubu: splitCSVParam(r.URL.Query().Get("urun_alt_grubu")),
|
|
Icerik: splitCSVParam(r.URL.Query().Get("icerik")),
|
|
Karisim: splitCSVParam(r.URL.Query().Get("karisim")),
|
|
Marka: splitCSVParam(r.URL.Query().Get("marka")),
|
|
}
|
|
if len(filters.UrunAnaGrubu) > 3 {
|
|
http.Error(w, "Urun Ana Grubu en fazla 3 secilebilir", http.StatusBadRequest)
|
|
return
|
|
}
|
|
includeTotal := true
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("include_total")); raw != "" {
|
|
if raw == "0" || strings.EqualFold(raw, "false") {
|
|
includeTotal = false
|
|
}
|
|
}
|
|
// When primary group filters are present, COUNT(*) is acceptable and improves UX
|
|
// (accurate totalCount/totalPages). Force includeTotal on.
|
|
if len(filters.UrunIlkGrubu) > 0 || len(filters.UrunAnaGrubu) > 0 {
|
|
includeTotal = true
|
|
}
|
|
|
|
sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by"))
|
|
desc := true
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("desc")); raw != "" {
|
|
if raw == "0" || strings.EqualFold(raw, "false") {
|
|
desc = false
|
|
}
|
|
}
|
|
pageResult, err := queries.GetProductPricingPage(ctx, page, limit, filters, includeTotal, sortBy, desc)
|
|
if err != nil {
|
|
if isPricingTimeoutLike(err, ctx.Err()) {
|
|
log.Printf(
|
|
"[ProductPricing] trace=%s timeout user=%s id=%d duration_ms=%d err=%v",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
time.Since(started).Milliseconds(),
|
|
err,
|
|
)
|
|
http.Error(w, "Urun fiyatlandirma listesi zaman asimina ugradi", http.StatusGatewayTimeout)
|
|
return
|
|
}
|
|
log.Printf(
|
|
"[ProductPricing] trace=%s query_error user=%s id=%d duration_ms=%d err=%v",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
time.Since(started).Milliseconds(),
|
|
err,
|
|
)
|
|
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
log.Printf(
|
|
"[ProductPricing] trace=%s success user=%s id=%d page=%d limit=%d count=%d total=%d total_pages=%d duration_ms=%d",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
pageResult.Page,
|
|
limit,
|
|
len(pageResult.Rows),
|
|
pageResult.TotalCount,
|
|
pageResult.TotalPages,
|
|
time.Since(started).Milliseconds(),
|
|
)
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("X-Total-Count", strconv.Itoa(pageResult.TotalCount))
|
|
w.Header().Set("X-Total-Pages", strconv.Itoa(pageResult.TotalPages))
|
|
w.Header().Set("X-Page", strconv.Itoa(pageResult.Page))
|
|
_ = json.NewEncoder(w).Encode(pageResult.Rows)
|
|
}
|
|
|
|
// GET /api/pricing/products/options?field=urunAnaGrubu&q=SER&limit=120
|
|
func GetProductPricingFilterOptionsHandler(w http.ResponseWriter, r *http.Request) {
|
|
started := time.Now()
|
|
traceID := buildPricingTraceID(r)
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
|
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
|
if !ok || claims == nil {
|
|
log.Printf("[ProductPricingOptions] trace=%s unauthorized method=%s path=%s", traceID, r.Method, r.URL.Path)
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second)
|
|
defer cancel()
|
|
|
|
field := strings.TrimSpace(r.URL.Query().Get("field"))
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
scopeUrunIlkGrubu := splitCSVParam(r.URL.Query().Get("urun_ilk_grubu"))
|
|
limit := 120
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 200 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
items, err := queries.GetProductPricingFilterOptions(ctx, field, q, limit, scopeUrunIlkGrubu)
|
|
if err != nil {
|
|
if isPricingTimeoutLike(err, ctx.Err()) {
|
|
log.Printf(
|
|
"[ProductPricingOptions] trace=%s timeout user=%s id=%d field=%s q=%s duration_ms=%d err=%v",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
field,
|
|
q,
|
|
time.Since(started).Milliseconds(),
|
|
err,
|
|
)
|
|
http.Error(w, "Urun fiyatlandirma filtre secenekleri zaman asimina ugradi", http.StatusGatewayTimeout)
|
|
return
|
|
}
|
|
log.Printf(
|
|
"[ProductPricingOptions] trace=%s query_error user=%s id=%d field=%s q=%s duration_ms=%d err=%v",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
field,
|
|
q,
|
|
time.Since(started).Milliseconds(),
|
|
err,
|
|
)
|
|
http.Error(w, "Filtre secenekleri alinamadi: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
type optionItem struct {
|
|
Label string `json:"label"`
|
|
Value string `json:"value"`
|
|
}
|
|
resp := struct {
|
|
Field string `json:"field"`
|
|
Count int `json:"count"`
|
|
Items []optionItem `json:"items"`
|
|
}{
|
|
Field: field,
|
|
Count: len(items),
|
|
Items: make([]optionItem, 0, len(items)),
|
|
}
|
|
for _, v := range items {
|
|
resp.Items = append(resp.Items, optionItem{Label: v, Value: v})
|
|
}
|
|
|
|
log.Printf(
|
|
"[ProductPricingOptions] trace=%s success user=%s id=%d field=%s q=%s count=%d duration_ms=%d",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
field,
|
|
q,
|
|
len(items),
|
|
time.Since(started).Milliseconds(),
|
|
)
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// GET /api/pricing/products/export-all
|
|
func ExportAllProductPricingHandler(w http.ResponseWriter, r *http.Request) {
|
|
started := time.Now()
|
|
traceID := buildPricingTraceID(r)
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
|
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
|
if !ok || claims == nil {
|
|
log.Printf("[ProductPricingExport] trace=%s unauthorized method=%s path=%s", traceID, r.Method, r.URL.Path)
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 170*time.Second)
|
|
defer cancel()
|
|
|
|
filters := parseProductPricingFilters(r)
|
|
if len(filters.UrunAnaGrubu) > 3 {
|
|
http.Error(w, "Urun Ana Grubu en fazla 3 secilebilir", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by"))
|
|
desc := true
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("desc")); raw != "" {
|
|
if raw == "0" || strings.EqualFold(raw, "false") {
|
|
desc = false
|
|
}
|
|
}
|
|
|
|
rows, err := queries.GetAllProductPricingRows(ctx, 1000, filters, sortBy, desc)
|
|
if err != nil {
|
|
if isPricingTimeoutLike(err, ctx.Err()) {
|
|
http.Error(w, "Urun fiyatlandirma export zaman asimina ugradi", http.StatusGatewayTimeout)
|
|
return
|
|
}
|
|
http.Error(w, "Urun fiyatlandirma export alinamadi: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
rows = filterProductPricingExportRows(rows, parseProductPricingExportFilters(r))
|
|
priceFields := parseExportPriceFields(r.URL.Query().Get("price_fields"))
|
|
var content string
|
|
if len(priceFields) > 0 {
|
|
content = buildProductPricingExportCSVWithPriceFields(rows, priceFields)
|
|
} else {
|
|
currencies := parseExportCurrencies(r.URL.Query().Get("currencies"))
|
|
content = buildProductPricingExportCSV(rows, currencies)
|
|
}
|
|
filename := fmt.Sprintf("product_pricing_all_%s.csv", time.Now().Format("2006-01-02"))
|
|
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
|
_, _ = w.Write([]byte("\uFEFF"))
|
|
_, _ = w.Write([]byte(content))
|
|
|
|
log.Printf(
|
|
"[ProductPricingExport] trace=%s success user=%s id=%d rows=%d duration_ms=%d",
|
|
traceID,
|
|
claims.Username,
|
|
claims.ID,
|
|
len(rows),
|
|
time.Since(started).Milliseconds(),
|
|
)
|
|
}
|
|
|
|
type productPricingExportFilters struct {
|
|
BrandGroupSelection []string
|
|
ValueFilters map[string][]string
|
|
StockQtyMin *float64
|
|
StockQtyMax *float64
|
|
StockEntryFrom string
|
|
StockEntryTo string
|
|
LastPricingFrom string
|
|
LastPricingTo string
|
|
}
|
|
|
|
func parseProductPricingFilters(r *http.Request) queries.ProductPricingFilters {
|
|
return queries.ProductPricingFilters{
|
|
Search: strings.TrimSpace(r.URL.Query().Get("q")),
|
|
ProductCode: splitCSVParam(r.URL.Query().Get("product_code")),
|
|
BrandGroup: splitCSVParam(r.URL.Query().Get("brand_group_selection")),
|
|
AskiliYan: splitCSVParam(r.URL.Query().Get("askili_yan")),
|
|
Kategori: splitCSVParam(r.URL.Query().Get("kategori")),
|
|
UrunIlkGrubu: splitCSVParam(r.URL.Query().Get("urun_ilk_grubu")),
|
|
UrunAnaGrubu: splitCSVParam(r.URL.Query().Get("urun_ana_grubu")),
|
|
UrunAltGrubu: splitCSVParam(r.URL.Query().Get("urun_alt_grubu")),
|
|
Icerik: splitCSVParam(r.URL.Query().Get("icerik")),
|
|
Karisim: splitCSVParam(r.URL.Query().Get("karisim")),
|
|
Marka: splitCSVParam(r.URL.Query().Get("marka")),
|
|
}
|
|
}
|
|
|
|
func parseProductPricingExportFilters(r *http.Request) productPricingExportFilters {
|
|
parseFloatPtr := func(raw string) *float64 {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &v
|
|
}
|
|
|
|
return productPricingExportFilters{
|
|
BrandGroupSelection: splitCSVParam(r.URL.Query().Get("brand_group_selection_local")),
|
|
ValueFilters: map[string][]string{
|
|
"costPrice": splitCSVParam(r.URL.Query().Get("vf_costPrice")),
|
|
"expenseForBasePrice": splitCSVParam(r.URL.Query().Get("vf_expenseForBasePrice")),
|
|
"basePriceUsd": splitCSVParam(r.URL.Query().Get("vf_basePriceUsd")),
|
|
"basePriceTry": splitCSVParam(r.URL.Query().Get("vf_basePriceTry")),
|
|
"usd1": splitCSVParam(r.URL.Query().Get("vf_usd1")),
|
|
"usd2": splitCSVParam(r.URL.Query().Get("vf_usd2")),
|
|
"usd3": splitCSVParam(r.URL.Query().Get("vf_usd3")),
|
|
"usd4": splitCSVParam(r.URL.Query().Get("vf_usd4")),
|
|
"usd5": splitCSVParam(r.URL.Query().Get("vf_usd5")),
|
|
"usd6": splitCSVParam(r.URL.Query().Get("vf_usd6")),
|
|
"eur1": splitCSVParam(r.URL.Query().Get("vf_eur1")),
|
|
"eur2": splitCSVParam(r.URL.Query().Get("vf_eur2")),
|
|
"eur3": splitCSVParam(r.URL.Query().Get("vf_eur3")),
|
|
"eur4": splitCSVParam(r.URL.Query().Get("vf_eur4")),
|
|
"eur5": splitCSVParam(r.URL.Query().Get("vf_eur5")),
|
|
"eur6": splitCSVParam(r.URL.Query().Get("vf_eur6")),
|
|
"try1": splitCSVParam(r.URL.Query().Get("vf_try1")),
|
|
"try2": splitCSVParam(r.URL.Query().Get("vf_try2")),
|
|
"try3": splitCSVParam(r.URL.Query().Get("vf_try3")),
|
|
"try4": splitCSVParam(r.URL.Query().Get("vf_try4")),
|
|
"try5": splitCSVParam(r.URL.Query().Get("vf_try5")),
|
|
"try6": splitCSVParam(r.URL.Query().Get("vf_try6")),
|
|
},
|
|
StockQtyMin: parseFloatPtr(r.URL.Query().Get("stock_qty_min")),
|
|
StockQtyMax: parseFloatPtr(r.URL.Query().Get("stock_qty_max")),
|
|
StockEntryFrom: strings.TrimSpace(r.URL.Query().Get("stock_entry_from")),
|
|
StockEntryTo: strings.TrimSpace(r.URL.Query().Get("stock_entry_to")),
|
|
LastPricingFrom: strings.TrimSpace(r.URL.Query().Get("last_pricing_from")),
|
|
LastPricingTo: strings.TrimSpace(r.URL.Query().Get("last_pricing_to")),
|
|
}
|
|
}
|
|
|
|
func filterProductPricingExportRows(rows []models.ProductPricing, f productPricingExportFilters) []models.ProductPricing {
|
|
if len(rows) == 0 {
|
|
return rows
|
|
}
|
|
|
|
out := make([]models.ProductPricing, 0, len(rows))
|
|
for _, row := range rows {
|
|
if len(f.BrandGroupSelection) > 0 && !containsText(f.BrandGroupSelection, row.BrandGroupSec) {
|
|
continue
|
|
}
|
|
if !matchesProductPricingValueFilters(row, f.ValueFilters) {
|
|
continue
|
|
}
|
|
if f.StockQtyMin != nil && row.StockQty < *f.StockQtyMin {
|
|
continue
|
|
}
|
|
if f.StockQtyMax != nil && row.StockQty > *f.StockQtyMax {
|
|
continue
|
|
}
|
|
if !matchesDateRangeString(row.StockEntryDate, f.StockEntryFrom, f.StockEntryTo) {
|
|
continue
|
|
}
|
|
if !matchesDateRangeString(row.LastPricingDate, f.LastPricingFrom, f.LastPricingTo) {
|
|
continue
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseExportCurrencies(raw string) []string {
|
|
requested := splitCSVParam(raw)
|
|
if len(requested) == 0 {
|
|
return []string{"USD", "EUR", "TRY"}
|
|
}
|
|
out := make([]string, 0, 3)
|
|
seen := map[string]bool{}
|
|
for _, item := range requested {
|
|
cur := strings.ToUpper(strings.TrimSpace(item))
|
|
if cur != "USD" && cur != "EUR" && cur != "TRY" {
|
|
continue
|
|
}
|
|
if seen[cur] {
|
|
continue
|
|
}
|
|
seen[cur] = true
|
|
out = append(out, cur)
|
|
}
|
|
if len(out) == 0 {
|
|
return []string{"USD", "EUR", "TRY"}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseExportPriceFields(raw string) []string {
|
|
requested := splitCSVParam(raw)
|
|
if len(requested) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, 18)
|
|
seen := map[string]bool{}
|
|
for _, item := range requested {
|
|
v := strings.ToLower(strings.TrimSpace(item))
|
|
if v == "" {
|
|
continue
|
|
}
|
|
// Accept both "usd1" and "USD1" etc.
|
|
switch v {
|
|
case "usd1", "usd2", "usd3", "usd4", "usd5", "usd6",
|
|
"eur1", "eur2", "eur3", "eur4", "eur5", "eur6",
|
|
"try1", "try2", "try3", "try4", "try5", "try6":
|
|
// ok
|
|
default:
|
|
continue
|
|
}
|
|
if seen[v] {
|
|
continue
|
|
}
|
|
seen[v] = true
|
|
out = append(out, v)
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func matchesProductPricingValueFilters(row models.ProductPricing, filters map[string][]string) bool {
|
|
if len(filters) == 0 {
|
|
return true
|
|
}
|
|
for field, selected := range filters {
|
|
if len(selected) == 0 {
|
|
continue
|
|
}
|
|
if !containsText(selected, productPricingValueKey(row, field)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func productPricingValueKey(row models.ProductPricing, field string) string {
|
|
value := 0.0
|
|
switch field {
|
|
case "costPrice":
|
|
value = row.CostPrice
|
|
case "expenseForBasePrice":
|
|
value = 0
|
|
case "basePriceUsd":
|
|
value = row.BasePriceUsd
|
|
case "basePriceTry":
|
|
value = row.BasePriceTry
|
|
case "usd1":
|
|
value = row.USD1
|
|
case "usd2":
|
|
value = row.USD2
|
|
case "usd3":
|
|
value = row.USD3
|
|
case "usd4":
|
|
value = row.USD4
|
|
case "usd5":
|
|
value = row.USD5
|
|
case "usd6":
|
|
value = row.USD6
|
|
case "eur1":
|
|
value = row.EUR1
|
|
case "eur2":
|
|
value = row.EUR2
|
|
case "eur3":
|
|
value = row.EUR3
|
|
case "eur4":
|
|
value = row.EUR4
|
|
case "eur5":
|
|
value = row.EUR5
|
|
case "eur6":
|
|
value = row.EUR6
|
|
case "try1":
|
|
value = row.TRY1
|
|
case "try2":
|
|
value = row.TRY2
|
|
case "try3":
|
|
value = row.TRY3
|
|
case "try4":
|
|
value = row.TRY4
|
|
case "try5":
|
|
value = row.TRY5
|
|
case "try6":
|
|
value = row.TRY6
|
|
}
|
|
return fmt.Sprintf("%.2f", value)
|
|
}
|
|
|
|
func matchesDateRangeString(value string, from string, to string) bool {
|
|
value = strings.TrimSpace(value)
|
|
from = strings.TrimSpace(from)
|
|
to = strings.TrimSpace(to)
|
|
if from == "" && to == "" {
|
|
return true
|
|
}
|
|
if value == "" {
|
|
return false
|
|
}
|
|
if from != "" && value < from {
|
|
return false
|
|
}
|
|
if to != "" && value > to {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func containsText(list []string, value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
for _, item := range list {
|
|
if strings.TrimSpace(item) == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func csvEscape(value string) string {
|
|
text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ")
|
|
if strings.Contains(text, ";") || strings.Contains(text, "\"") {
|
|
text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
|
|
}
|
|
return text
|
|
}
|
|
|
|
func csvFloat(value float64) string {
|
|
return fmt.Sprintf("%.2f", value)
|
|
}
|
|
|
|
func exportPriceFieldTitle(field string) string {
|
|
field = strings.ToLower(strings.TrimSpace(field))
|
|
if len(field) != 4 {
|
|
return strings.ToUpper(field)
|
|
}
|
|
cur := strings.ToUpper(field[:3])
|
|
lv := field[3:]
|
|
return fmt.Sprintf("%s %s", cur, lv)
|
|
}
|
|
|
|
func exportPriceFieldValue(row models.ProductPricing, field string) float64 {
|
|
switch strings.ToLower(strings.TrimSpace(field)) {
|
|
case "usd1":
|
|
return row.USD1
|
|
case "usd2":
|
|
return row.USD2
|
|
case "usd3":
|
|
return row.USD3
|
|
case "usd4":
|
|
return row.USD4
|
|
case "usd5":
|
|
return row.USD5
|
|
case "usd6":
|
|
return row.USD6
|
|
case "eur1":
|
|
return row.EUR1
|
|
case "eur2":
|
|
return row.EUR2
|
|
case "eur3":
|
|
return row.EUR3
|
|
case "eur4":
|
|
return row.EUR4
|
|
case "eur5":
|
|
return row.EUR5
|
|
case "eur6":
|
|
return row.EUR6
|
|
case "try1":
|
|
return row.TRY1
|
|
case "try2":
|
|
return row.TRY2
|
|
case "try3":
|
|
return row.TRY3
|
|
case "try4":
|
|
return row.TRY4
|
|
case "try5":
|
|
return row.TRY5
|
|
case "try6":
|
|
return row.TRY6
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func buildProductPricingExportCSVWithPriceFields(rows []models.ProductPricing, priceFields []string) string {
|
|
var b strings.Builder
|
|
|
|
headers := []string{
|
|
"MARKA GRUBU SECIMI",
|
|
"MARKA",
|
|
"URUN KODU",
|
|
"STOK ADET",
|
|
"STOK GIRIS TARIHI",
|
|
"SON MALIYETLENDIRME",
|
|
"SON FIYATLANDIRMA TARIHI",
|
|
"ASKILI YAN",
|
|
"KATEGORI",
|
|
"URUN ILK GRUBU",
|
|
"URUN ANA GRUBU",
|
|
"URUN ALT GRUBU",
|
|
"ICERIK",
|
|
"KARISIM",
|
|
"MALIYET FIYATI",
|
|
"TABAN FIYAT MASRAF",
|
|
"TABAN USD",
|
|
"TABAN TRY",
|
|
}
|
|
for _, h := range headers {
|
|
b.WriteString(csvEscape(h))
|
|
b.WriteString(";")
|
|
}
|
|
for i, pf := range priceFields {
|
|
b.WriteString(csvEscape(exportPriceFieldTitle(pf)))
|
|
if i == len(priceFields)-1 {
|
|
b.WriteString("\n")
|
|
} else {
|
|
b.WriteString(";")
|
|
}
|
|
}
|
|
|
|
for _, row := range rows {
|
|
base := []string{
|
|
row.BrandGroupSec,
|
|
row.Marka,
|
|
row.ProductCode,
|
|
csvFloat(row.StockQty),
|
|
row.StockEntryDate,
|
|
row.LastCostingDate,
|
|
row.LastPricingDate,
|
|
row.AskiliYan,
|
|
row.Kategori,
|
|
row.UrunIlkGrubu,
|
|
row.UrunAnaGrubu,
|
|
row.UrunAltGrubu,
|
|
row.Icerik,
|
|
row.Karisim,
|
|
csvFloat(row.CostPrice),
|
|
csvFloat(0),
|
|
csvFloat(row.BasePriceUsd),
|
|
csvFloat(row.BasePriceTry),
|
|
}
|
|
for _, item := range base {
|
|
b.WriteString(csvEscape(item))
|
|
b.WriteString(";")
|
|
}
|
|
for i, pf := range priceFields {
|
|
b.WriteString(csvEscape(csvFloat(exportPriceFieldValue(row, pf))))
|
|
if i == len(priceFields)-1 {
|
|
b.WriteString("\n")
|
|
} else {
|
|
b.WriteString(";")
|
|
}
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func buildProductPricingExportCSV(rows []models.ProductPricing, currencies []string) string {
|
|
// Backward compatible export: USD/EUR/TRY and all 1..6 tiers per currency.
|
|
fields := make([]string, 0, 18)
|
|
for _, cur := range currencies {
|
|
cur = strings.ToUpper(strings.TrimSpace(cur))
|
|
switch cur {
|
|
case "USD", "EUR", "TRY":
|
|
default:
|
|
continue
|
|
}
|
|
for lv := 1; lv <= 6; lv++ {
|
|
fields = append(fields, strings.ToLower(fmt.Sprintf("%s%d", cur, lv)))
|
|
}
|
|
}
|
|
if len(fields) == 0 {
|
|
fields = []string{"usd1", "usd2", "usd3", "usd4", "usd5", "usd6", "eur1", "eur2", "eur3", "eur4", "eur5", "eur6", "try1", "try2", "try3", "try4", "try5", "try6"}
|
|
}
|
|
return buildProductPricingExportCSVWithPriceFields(rows, fields)
|
|
}
|
|
|
|
func productPricingCurrencyValues(row models.ProductPricing, currency string) []float64 {
|
|
switch currency {
|
|
case "USD":
|
|
return []float64{row.USD1, row.USD2, row.USD3, row.USD4, row.USD5, row.USD6}
|
|
case "EUR":
|
|
return []float64{row.EUR1, row.EUR2, row.EUR3, row.EUR4, row.EUR5, row.EUR6}
|
|
default:
|
|
return []float64{row.TRY1, row.TRY2, row.TRY3, row.TRY4, row.TRY5, row.TRY6}
|
|
}
|
|
}
|
|
|
|
func buildPricingTraceID(r *http.Request) string {
|
|
if r != nil {
|
|
if id := strings.TrimSpace(r.Header.Get("X-Request-ID")); id != "" {
|
|
return id
|
|
}
|
|
if id := strings.TrimSpace(r.Header.Get("X-Correlation-ID")); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
return "pricing-" + strconv.FormatInt(time.Now().UnixNano(), 36)
|
|
}
|
|
|
|
func isPricingTimeoutLike(err error, ctxErr error) bool {
|
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctxErr, context.DeadlineExceeded) {
|
|
return true
|
|
}
|
|
if err == nil {
|
|
return false
|
|
}
|
|
e := strings.ToLower(err.Error())
|
|
return strings.Contains(e, "timeout") ||
|
|
strings.Contains(e, "i/o timeout") ||
|
|
strings.Contains(e, "wsarecv") ||
|
|
strings.Contains(e, "connection attempt failed") ||
|
|
strings.Contains(e, "no connection could be made") ||
|
|
strings.Contains(e, "failed to respond")
|
|
}
|
|
|
|
func splitCSVParam(raw string) []string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out
|
|
}
|