506 lines
15 KiB
Go
506 lines
15 KiB
Go
package routes
|
|
|
|
import (
|
|
"bssapp-backend/auth"
|
|
"bssapp-backend/db"
|
|
"bssapp-backend/queries"
|
|
"bssapp-backend/utils"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
type productPricingHistoryPGRow struct {
|
|
ID string `json:"id"`
|
|
Currency string `json:"currency"`
|
|
LevelNo int `json:"level_no"`
|
|
Price float64 `json:"price"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
SdprcGrpID int `json:"sdprcgrp_id"`
|
|
}
|
|
|
|
type productPricingHistoryMSSQLRow struct {
|
|
PriceListLineID string `json:"price_list_line_id"`
|
|
Currency string `json:"currency"`
|
|
PriceGroupCode string `json:"price_group_code"`
|
|
Price float64 `json:"price"`
|
|
ValidDate string `json:"valid_date"`
|
|
ValidTime string `json:"valid_time"`
|
|
LastUpdatedDate string `json:"last_updated_date"`
|
|
IsDisabled bool `json:"is_disabled"`
|
|
}
|
|
|
|
type productPricingHistoryResponse struct {
|
|
ProductCode string `json:"product_code"`
|
|
Postgres []productPricingHistoryPGRow `json:"postgres"`
|
|
Mssql []productPricingHistoryMSSQLRow `json:"mssql"`
|
|
}
|
|
|
|
func GetProductPricingHistoryHandler(pg *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
|
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
|
if !ok || claims == nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
productCode := strings.TrimSpace(mux.Vars(r)["code"])
|
|
if productCode == "" {
|
|
http.Error(w, "product code required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Load nebim price groups from PG mapping (18) + base groups (2).
|
|
priceGroups := []string{"TM-USD", "TM-TRY"}
|
|
if pg != nil {
|
|
rows, err := pg.QueryContext(ctx, `
|
|
SELECT DISTINCT COALESCE(NULLIF(BTRIM(price_group_code), ''), '')
|
|
FROM mk_price_target_map_nebim
|
|
WHERE is_active = TRUE
|
|
`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var code string
|
|
if err := rows.Scan(&code); err != nil {
|
|
_ = rows.Close()
|
|
break
|
|
}
|
|
code = strings.TrimSpace(code)
|
|
if code != "" {
|
|
priceGroups = append(priceGroups, code)
|
|
}
|
|
}
|
|
_ = rows.Close()
|
|
}
|
|
}
|
|
|
|
resp := productPricingHistoryResponse{
|
|
ProductCode: productCode,
|
|
Postgres: []productPricingHistoryPGRow{},
|
|
Mssql: []productPricingHistoryMSSQLRow{},
|
|
}
|
|
|
|
// Postgres sdprc history.
|
|
if pg != nil {
|
|
pgRows, err := pg.QueryContext(ctx, `
|
|
SELECT
|
|
sdprc.id::text,
|
|
sdprc.crn,
|
|
sdprc.sdprcgrp_id,
|
|
COALESCE(sdprc.prc, 0)::float8,
|
|
TO_CHAR(sdprc.zlins_dttm, 'YYYY-MM-DD HH24:MI:SS')
|
|
FROM sdprc
|
|
JOIN mmitem ON mmitem.id = sdprc.mmitem_id
|
|
WHERE mmitem.code = $1
|
|
AND sdprc.crn IN ('USD','EUR','TRY')
|
|
AND sdprc.sdprcgrp_id BETWEEN 1 AND 6
|
|
ORDER BY sdprc.zlins_dttm DESC
|
|
LIMIT 400;
|
|
`, productCode)
|
|
if err == nil {
|
|
for pgRows.Next() {
|
|
var id, cur, at string
|
|
var grp int
|
|
var prc float64
|
|
if err := pgRows.Scan(&id, &cur, &grp, &prc, &at); err != nil {
|
|
_ = pgRows.Close()
|
|
http.Error(w, "pg history scan error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
resp.Postgres = append(resp.Postgres, productPricingHistoryPGRow{
|
|
ID: strings.TrimSpace(id),
|
|
Currency: strings.ToUpper(strings.TrimSpace(cur)),
|
|
SdprcGrpID: grp,
|
|
LevelNo: grp,
|
|
Price: prc,
|
|
UpdatedAt: strings.TrimSpace(at),
|
|
})
|
|
}
|
|
_ = pgRows.Close()
|
|
}
|
|
}
|
|
|
|
// MSSQL trPriceListLine history (only relevant price groups).
|
|
mssql := db.GetDB()
|
|
if mssql != nil {
|
|
// Build a safe "IN" via OR parameters.
|
|
conds := make([]string, 0, len(priceGroups))
|
|
args := make([]any, 0, len(priceGroups)+1)
|
|
args = append(args, sql.Named("p1", productCode))
|
|
for i, g := range priceGroups {
|
|
name := fmt.Sprintf("g%d", i+1)
|
|
conds = append(conds, "LTRIM(RTRIM(p.PriceGroupCode)) = @"+name)
|
|
args = append(args, sql.Named(name, g))
|
|
}
|
|
wherePG := "1=0"
|
|
if len(conds) > 0 {
|
|
wherePG = "(" + strings.Join(conds, " OR ") + ")"
|
|
}
|
|
q := `
|
|
SELECT TOP (400)
|
|
CONVERT(NVARCHAR(36), p.PriceListLineID) AS PriceListLineID,
|
|
LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode,
|
|
LTRIM(RTRIM(p.PriceGroupCode)) AS PriceGroupCode,
|
|
CAST(p.Price AS FLOAT) AS Price,
|
|
CONVERT(VARCHAR(10), p.ValidDate, 23) AS ValidDate,
|
|
CONVERT(VARCHAR(8), p.ValidTime, 108) AS ValidTime,
|
|
CONVERT(VARCHAR(19), p.LastUpdatedDate, 120) AS LastUpdatedDate,
|
|
CAST(ISNULL(p.IsDisabled, 0) AS BIT) AS IsDisabled
|
|
FROM dbo.trPriceListLine p WITH(NOLOCK)
|
|
WHERE p.ItemTypeCode = 1
|
|
AND LTRIM(RTRIM(p.ItemCode)) = @p1
|
|
AND ` + wherePG + `
|
|
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC;
|
|
`
|
|
rows, err := mssql.QueryContext(ctx, q, args...)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var id, cur, grp, vd, vt, lud string
|
|
var prc float64
|
|
var disabled bool
|
|
if err := rows.Scan(&id, &cur, &grp, &prc, &vd, &vt, &lud, &disabled); err != nil {
|
|
_ = rows.Close()
|
|
http.Error(w, "mssql history scan error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
resp.Mssql = append(resp.Mssql, productPricingHistoryMSSQLRow{
|
|
PriceListLineID: strings.TrimSpace(id),
|
|
Currency: strings.ToUpper(strings.TrimSpace(cur)),
|
|
PriceGroupCode: strings.TrimSpace(grp),
|
|
Price: prc,
|
|
ValidDate: strings.TrimSpace(vd),
|
|
ValidTime: strings.TrimSpace(vt),
|
|
LastUpdatedDate: strings.TrimSpace(lud),
|
|
IsDisabled: disabled,
|
|
})
|
|
}
|
|
_ = rows.Close()
|
|
}
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
}
|
|
}
|
|
|
|
type deleteLatestPriceHistoryRequest struct {
|
|
DeletePostgres bool `json:"delete_postgres"`
|
|
DeleteMssql bool `json:"delete_mssql"`
|
|
Currency string `json:"currency"` // USD/EUR/TRY
|
|
LevelNo int `json:"level_no"` // 1..6 (tier); for base use 0
|
|
IsBase bool `json:"is_base"`
|
|
PriceGroupCode string `json:"price_group_code"` // optional override for MSSQL deletes
|
|
}
|
|
|
|
func PostDeleteLatestProductPriceHistoryHandler(pg *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
|
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
|
if !ok || claims == nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
productCode := strings.TrimSpace(mux.Vars(r)["code"])
|
|
if productCode == "" {
|
|
http.Error(w, "product code required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var req deleteLatestPriceHistoryRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !req.DeletePostgres && !req.DeleteMssql {
|
|
req.DeletePostgres = true
|
|
req.DeleteMssql = true
|
|
}
|
|
|
|
cur := strings.ToUpper(strings.TrimSpace(req.Currency))
|
|
if cur != "USD" && cur != "EUR" && cur != "TRY" {
|
|
http.Error(w, "invalid currency", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !req.IsBase && req.DeletePostgres && (req.LevelNo < 1 || req.LevelNo > 6) {
|
|
http.Error(w, "invalid level_no", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// PG delete (sdprc).
|
|
deletedPG := int64(0)
|
|
if req.DeletePostgres && !req.IsBase && pg != nil {
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
http.Error(w, "pg tx error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
var mmItemID int64
|
|
if err := tx.QueryRowContext(ctx, `SELECT id FROM mmitem WHERE code=$1`, productCode).Scan(&mmItemID); err != nil {
|
|
http.Error(w, "pg product not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
grp := req.LevelNo
|
|
// Delete latest row for that currency+level.
|
|
res, err := tx.ExecContext(ctx, `
|
|
DELETE FROM sdprc
|
|
WHERE id = (
|
|
SELECT id
|
|
FROM sdprc
|
|
WHERE mmitem_id=$1 AND crn=$2 AND sdprcgrp_id=$3
|
|
ORDER BY zlins_dttm DESC
|
|
LIMIT 1
|
|
);
|
|
`, mmItemID, cur, grp)
|
|
if err != nil {
|
|
http.Error(w, "pg delete error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
deletedPG, _ = res.RowsAffected()
|
|
|
|
// enqueue delta recompute for this product to keep derived currencies consistent
|
|
_, _ = queries.EnqueuePriceRecalc(ctx, tx, []string{productCode}, "history_delete")
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
http.Error(w, "pg commit error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// MSSQL delete (trPriceListLine).
|
|
deletedMSSQL := int64(0)
|
|
if req.DeleteMssql {
|
|
mssql := db.GetDB()
|
|
if mssql == nil {
|
|
http.Error(w, "mssql not connected", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
tx, err := mssql.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
http.Error(w, "mssql tx error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
priceGroup := strings.TrimSpace(req.PriceGroupCode)
|
|
if req.IsBase {
|
|
if cur == "USD" {
|
|
priceGroup = "TM-USD"
|
|
} else if cur == "TRY" {
|
|
priceGroup = "TM-TRY"
|
|
} else {
|
|
http.Error(w, "base only supports USD/TRY", http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else if priceGroup == "" && pg != nil {
|
|
_ = pg.QueryRowContext(ctx, `
|
|
SELECT COALESCE(NULLIF(BTRIM(price_group_code), ''), '')
|
|
FROM mk_price_target_map_nebim
|
|
WHERE is_active=TRUE AND currency=$1 AND level_no=$2
|
|
`, cur, req.LevelNo).Scan(&priceGroup)
|
|
}
|
|
priceGroup = strings.TrimSpace(priceGroup)
|
|
if priceGroup == "" {
|
|
http.Error(w, "missing price group mapping", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
res, err := tx.ExecContext(ctx, `
|
|
;WITH latest AS (
|
|
SELECT TOP (1) p.PriceListLineID
|
|
FROM dbo.trPriceListLine p WITH(UPDLOCK, ROWLOCK)
|
|
WHERE p.ItemTypeCode=1
|
|
AND LTRIM(RTRIM(p.ItemCode))=@p1
|
|
AND LTRIM(RTRIM(p.DocCurrencyCode))=@p2
|
|
AND LTRIM(RTRIM(p.PriceGroupCode))=@p3
|
|
AND ISNULL(p.IsDisabled, 0)=0
|
|
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC
|
|
)
|
|
DELETE FROM dbo.trPriceListLine
|
|
WHERE PriceListLineID IN (SELECT PriceListLineID FROM latest);
|
|
`, sql.Named("p1", productCode), sql.Named("p2", cur), sql.Named("p3", priceGroup))
|
|
if err != nil {
|
|
http.Error(w, "mssql delete error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
deletedMSSQL, _ = res.RowsAffected()
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
http.Error(w, "mssql commit error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"success": true,
|
|
"product_code": productCode,
|
|
"deleted_pg": deletedPG,
|
|
"deleted_mssql": deletedMSSQL,
|
|
"actor_user": claims.Username,
|
|
"actor_user_id": claims.ID,
|
|
})
|
|
}
|
|
}
|
|
|
|
type deleteSelectedPriceHistoryRequest struct {
|
|
PGIDs []string `json:"pg_ids"` // sdprc.id (uuid)
|
|
MSSQLIDs []string `json:"mssql_ids"` // trPriceListLine.PriceListLineID (uuid)
|
|
}
|
|
|
|
func PostDeleteSelectedProductPriceHistoryHandler(pg *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
w.Header().Set("X-Trace-ID", traceID)
|
|
|
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
|
if !ok || claims == nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
productCode := strings.TrimSpace(mux.Vars(r)["code"])
|
|
if productCode == "" {
|
|
http.Error(w, "product code required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var req deleteSelectedPriceHistoryRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// normalize ids
|
|
pgIDs := make([]string, 0, len(req.PGIDs))
|
|
for _, x := range req.PGIDs {
|
|
s := strings.TrimSpace(x)
|
|
if s != "" {
|
|
pgIDs = append(pgIDs, s)
|
|
}
|
|
}
|
|
msIDs := make([]string, 0, len(req.MSSQLIDs))
|
|
for _, x := range req.MSSQLIDs {
|
|
s := strings.TrimSpace(x)
|
|
if s != "" {
|
|
msIDs = append(msIDs, s)
|
|
}
|
|
}
|
|
if len(pgIDs) == 0 && len(msIDs) == 0 {
|
|
http.Error(w, "no ids selected", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
|
defer cancel()
|
|
|
|
deletedPG := int64(0)
|
|
if len(pgIDs) > 0 && pg != nil {
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
http.Error(w, "pg tx error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Resolve product id to constrain deletes to the given productCode.
|
|
var mmItemID int64
|
|
if err := tx.QueryRowContext(ctx, `SELECT id FROM mmitem WHERE code=$1`, productCode).Scan(&mmItemID); err != nil {
|
|
http.Error(w, "pg product not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Delete only rows matching mmitem_id + id list.
|
|
res, err := tx.ExecContext(ctx, `
|
|
DELETE FROM sdprc
|
|
WHERE mmitem_id = $1
|
|
AND id = ANY($2::uuid[]);
|
|
`, mmItemID, pq.Array(pgIDs))
|
|
if err != nil {
|
|
http.Error(w, "pg delete error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
deletedPG, _ = res.RowsAffected()
|
|
|
|
_, _ = queries.EnqueuePriceRecalc(ctx, tx, []string{productCode}, "history_delete_selected")
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
http.Error(w, "pg commit error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
deletedMSSQL := int64(0)
|
|
if len(msIDs) > 0 {
|
|
mssql := db.GetDB()
|
|
if mssql == nil {
|
|
http.Error(w, "mssql not connected", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
tx, err := mssql.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
http.Error(w, "mssql tx error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Build a safe IN-list via named parameters.
|
|
placeholders := make([]string, 0, len(msIDs))
|
|
args := make([]any, 0, len(msIDs)+1)
|
|
args = append(args, sql.Named("p1", productCode))
|
|
for i, id := range msIDs {
|
|
name := fmt.Sprintf("id%d", i+1)
|
|
placeholders = append(placeholders, "@"+name)
|
|
args = append(args, sql.Named(name, id))
|
|
}
|
|
|
|
q := `
|
|
DELETE FROM dbo.trPriceListLine
|
|
WHERE ItemTypeCode = 1
|
|
AND LTRIM(RTRIM(ItemCode)) = @p1
|
|
AND PriceListLineID IN (` + strings.Join(placeholders, ",") + `);
|
|
`
|
|
res, err := tx.ExecContext(ctx, q, args...)
|
|
if err != nil {
|
|
http.Error(w, "mssql delete error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
deletedMSSQL, _ = res.RowsAffected()
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
http.Error(w, "mssql commit error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"success": true,
|
|
"product_code": productCode,
|
|
"deleted_pg": deletedPG,
|
|
"deleted_mssql": deletedMSSQL,
|
|
"actor_user": claims.Username,
|
|
"actor_user_id": claims.ID,
|
|
})
|
|
}
|
|
}
|