Merge remote-tracking branch 'origin/master'
This commit is contained in:
505
svc/routes/product_pricing_history.go
Normal file
505
svc/routes/product_pricing_history.go
Normal file
@@ -0,0 +1,505 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user