This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

8
svc/.env Normal file
View File

@@ -0,0 +1,8 @@
JWT_SECRET=bssapp_super_secret_key_1234567890
PASSWORD_RESET_SECRET=1dc7d6d52fd0459a8b1f288a6590428e760f54339f8e47beb20db36b6df6070b
APP_FRONTEND_URL=http://localhost:9000
API_URL=http://localhost:8080

47
svc/auth/claims.go Normal file
View File

@@ -0,0 +1,47 @@
package auth
import (
"strings"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
// ==================================================
// 🔑 IDENTITY
// ==================================================
ID int64 `json:"id"`
Username string `json:"username"`
RoleID int64 `json:"role_id"`
RoleCode string `json:"role_code"`
DepartmentCodes []string `json:"department_codes"`
// ==================================================
// 🧾 NEBIM (frontend filtre & backend guard için)
// ==================================================
V3Username string `json:"v3_username"`
V3UserGroup string `json:"v3_usergroup"`
// ==================================================
// 🔐 SESSION
// ==================================================
SessionID string `json:"session_id"`
// ==================================================
// ⚠️ SECURITY
// ==================================================
ForcePasswordChange bool `json:"force_password_change"`
jwt.RegisteredClaims
}
func (c *Claims) IsAdmin() bool {
if c == nil {
return false
}
role := strings.ToLower(strings.TrimSpace(c.RoleCode))
return role == "admin"
}

36
svc/auth/claims_mapper.go Normal file
View File

@@ -0,0 +1,36 @@
package auth
import (
"time"
"bssapp-backend/models"
"github.com/golang-jwt/jwt/v5"
)
func BuildClaimsFromUser(u *models.MkUser, ttl time.Duration) Claims {
now := time.Now()
return Claims{
// 🔴 mk_dfusr.id
ID: u.ID,
Username: u.Username,
RoleCode: u.RoleCode,
RoleID: u.RoleID,
// ✅ BURASI
DepartmentCodes: u.DepartmentCodes,
SessionID: u.SessionID,
ForcePasswordChange: u.ForcePasswordChange,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "bssapp",
Subject: u.Username,
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
},
}
}

15
svc/auth/context.go Normal file
View File

@@ -0,0 +1,15 @@
package auth
import (
"bssapp-backend/ctxkeys"
"context"
)
func WithClaims(ctx context.Context, claims *Claims) context.Context {
return context.WithValue(ctx, ctxkeys.UserContextKey, claims)
}
func GetClaimsFromContext(ctx context.Context) (*Claims, bool) {
claims, ok := ctx.Value(ctxkeys.UserContextKey).(*Claims)
return claims, ok
}

53
svc/auth/jwt.go Normal file
View File

@@ -0,0 +1,53 @@
package auth
import (
"errors"
"os"
"github.com/golang-jwt/jwt/v5"
)
// package auth
func jwtSecret() ([]byte, error) {
sec := os.Getenv("JWT_SECRET")
if len(sec) < 10 {
return nil, errors.New("JWT_SECRET environment boş veya çok kısa")
}
return []byte(sec), nil
}
// ✅ TEK VE DOĞRU TOKEN ÜRETİCİ
func GenerateToken(claims Claims, username string, change bool) (string, error) {
secret, err := jwtSecret()
if err != nil {
return "", err
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
func ValidateToken(tokenStr string) (*Claims, error) {
secret, err := jwtSecret()
if err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(
tokenStr,
&Claims{},
func(token *jwt.Token) (interface{}, error) {
return secret, nil
},
)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("token geçersiz")
}
return claims, nil
}

44
svc/auth/logout.go Normal file
View File

@@ -0,0 +1,44 @@
package auth
import (
"bssapp-backend/internal/auditlog"
"bssapp-backend/repository"
"database/sql"
"encoding/json"
"net/http"
"time"
)
func LogoutAllHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
userID := claims.ID
_ = repository.NewRefreshTokenRepository(db).
RevokeAllForUser(userID)
http.SetCookie(w, &http.Cookie{
Name: "mk_refresh",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
})
auditlog.Write(auditlog.ActivityLog{
UserID: auditlog.IntUserIDToUUID(int(userID)),
ActionType: "logout_all",
ActionCategory: "auth",
Description: "user logged out from all devices",
IsSuccess: true,
})
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
}

1
svc/config/config.go Normal file
View File

@@ -0,0 +1 @@
package config

5
svc/ctxkeys/keys.go Normal file
View File

@@ -0,0 +1,5 @@
package ctxkeys
type ContextKey string
const UserContextKey ContextKey = "jwt_claims"

31
svc/db/mssql.go Normal file
View File

@@ -0,0 +1,31 @@
package db
import (
"database/sql"
"fmt"
"log"
_ "github.com/microsoft/go-mssqldb"
)
var MssqlDB *sql.DB
func ConnectMSSQL() {
connString := "sqlserver://sa:Gil_0150@10.0.0.9:1433?databaseName=BAGGI_V3"
var err error
MssqlDB, err = sql.Open("sqlserver", connString)
if err != nil {
log.Fatal("MSSQL bağlantı hatası:", err)
}
err = MssqlDB.Ping()
if err != nil {
log.Fatal("MSSQL erişilemiyor:", err)
}
fmt.Println("✅ MSSQL bağlantısı başarılı!")
}
func GetDB() *sql.DB {
return MssqlDB
}

68
svc/db/postgres.go Normal file
View File

@@ -0,0 +1,68 @@
package db
import (
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/lib/pq"
)
var PgDB *sql.DB
// ConnectPostgres → PostgreSQL veritabanına bağlanır
func ConnectPostgres() (*sql.DB, error) {
// Bağlantı stringi (istersen .envden oku)
connStr := os.Getenv("POSTGRES_CONN")
if connStr == "" {
// fallback → sabit tanımlı bağlantı
connStr = "host=172.16.0.3 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable"
}
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err)
}
// =======================================================
// 🔹 BAĞLANTI HAVUZU (AUDIT LOG UYUMLU)
// =======================================================
db.SetMaxOpenConns(30) // audit + api paralel çalışsın
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute) // 🔥 uzun idle audit bağlantılarını kapat
// 🔹 Test et
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("PostgreSQL erişilemiyor: %w", err)
}
log.Println("✅ PostgreSQL bağlantısı başarılı!")
PgDB = db
return db, nil
}
// GetPostgresUsers → test amaçlı ilk 5 kullanıcıyı listeler
func GetPostgresUsers(db *sql.DB) error {
query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5`
rows, err := db.Query(query)
if err != nil {
return fmt.Errorf("PostgreSQL sorgu hatası: %w", err)
}
defer rows.Close()
fmt.Println("📋 İlk 5 PostgreSQL kullanıcısı:")
for rows.Next() {
var id int
var code, email string
if err := rows.Scan(&id, &code, &email); err != nil {
return err
}
fmt.Printf(" ➜ ID: %-4d | USER: %-20s | EMAIL: %s\n", id, code, email)
}
return rows.Err()
}

0
svc/deneme Normal file
View File

Binary file not shown.

BIN
svc/fonts/DejaVuSans.ttf Normal file

Binary file not shown.

0
svc/fonts/FreeSans.ttf Normal file
View File

View File

30
svc/go.mod Normal file
View File

@@ -0,0 +1,30 @@
module bssapp-backend
go 1.24.0 // senin Go versiyonuna göre değişir
toolchain go1.24.5
require (
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/joho/godotenv v1.5.1
github.com/jung-kurt/gofpdf v1.16.2
github.com/lib/pq v1.10.9
github.com/microsoft/go-mssqldb v1.9.3
github.com/xuri/excelize/v2 v2.10.0
golang.org/x/crypto v0.43.0
golang.org/x/oauth2 v0.34.0
)
require (
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/text v0.30.0 // indirect
)

76
svc/go.sum Normal file
View File

@@ -0,0 +1,76 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,35 @@
package auditlog
import (
"context"
)
func ForcePasswordChangeStarted(
ctx context.Context,
targetUserID int64,
reason string, // admin_reset | login_enforced
) {
Write(ActivityLog{
UserID: IntUserIDToUUID(int(targetUserID)),
ActionType: "force_password_change_started",
ActionCategory: "auth",
Description: "kullanıcı için zorunlu parola değişimi başlatıldı",
IsSuccess: true,
ErrorMessage: reason,
})
}
func ForcePasswordChangeCompleted(
ctx context.Context,
userID int64,
source string, // reset_link | self_change | admin_reset
) {
Write(ActivityLog{
UserID: IntUserIDToUUID(int(userID)),
ActionType: "force_password_change_completed",
ActionCategory: "auth",
Description: "kullanıcı parolasını başarıyla güncelledi",
IsSuccess: true,
ErrorMessage: source,
})
}

View File

@@ -0,0 +1,73 @@
package auditlog
import (
"crypto/md5"
"fmt"
"time"
)
//
// =======================================================
// 🕵️ AUDIT LOG — HELPER FUNCTIONS
// =======================================================
// Bu dosya:
// - UUID bekleyen kolonlar için int → uuid dönüşümü
// - NULL-safe insert yardımcıları
// içerir
//
// -------------------------------------------------------
// 🔹 IntUserIDToUUID
// -------------------------------------------------------
// int user_id → deterministic UUID
// PostgreSQL uuid kolonu ile %100 uyumlu
//
// Aynı userID → her zaman aynı UUID
func IntUserIDToUUID(userID int) string {
if userID <= 0 {
return ""
}
sum := md5.Sum([]byte(fmt.Sprintf("bssapp-user-%d", userID)))
return fmt.Sprintf(
"%x-%x-%x-%x-%x",
sum[0:4],
sum[4:6],
sum[6:8],
sum[8:10],
sum[10:16],
)
}
// -------------------------------------------------------
// 🔹 nullIfZeroTime
// -------------------------------------------------------
// Zero time → NULL (SQL uyumlu)
func nullIfZeroTime(t time.Time) interface{} {
if t.IsZero() {
return nil
}
return t
}
// -------------------------------------------------------
// 🔹 nullIfZeroInt
// -------------------------------------------------------
// 0 → NULL (SQL uyumlu)
func nullIfZeroInt(v int) interface{} {
if v == 0 {
return nil
}
return v
}
func Int64UserIDToUUID(id int64) string {
return IntUserIDToUUID(int(id))
}
func nullIfEmpty(s string) any {
if s == "" {
return nil
}
return s
}

View File

@@ -0,0 +1,30 @@
package auditlog
import (
"database/sql"
"log"
"sync"
)
var (
logQueue chan ActivityLog
dbConn *sql.DB
once sync.Once
)
// Init → main.go içinden çağrılacak (tek sefer)
func Init(db *sql.DB, bufferSize int) {
log.Println("🟢 auditlog Init called, buffer:", bufferSize)
dbConn = db
logQueue = make(chan ActivityLog, bufferSize)
go logWorker()
}
// Optional: app kapanırken flush/stop istersen
func Close() {
if logQueue != nil {
close(logQueue)
}
}

View File

@@ -0,0 +1,37 @@
package auditlog
import "time"
type ActivityLog struct {
// identity
UserID string // UUID (auth)
DfUsrID int64 // DF user id (mk_dfusr.id)
Username string
RoleCode string
// action
ActionType string
ActionCategory string
ActionTarget string
Description string
// tech
IpAddress string
UserAgent string
SessionID string
// timing
RequestStartedAt time.Time
RequestFinishedAt time.Time
DurationMs int
HttpStatus int
// result
IsSuccess bool
ErrorMessage string
TargetDfUsrID int64
TargetUsername string
ChangeBefore any // map[string]any
ChangeAfter any
}

View File

@@ -0,0 +1,141 @@
package auditlog
import (
"encoding/json"
"log"
)
func toJSONB(v any) any {
if v == nil {
return nil
}
b, err := json.Marshal(v)
if err != nil {
// JSON marshal hata olursa logu bozmayalım
log.Println("⚠️ auditlog json marshal error:", err)
return nil
}
return b // pq jsonb için []byte kabul eder
}
func logWorker() {
log.Println("🟢 auditlog worker STARTED")
for entry := range logQueue {
// ---------- DFUSR_ID ----------
var dfusrID any
if entry.DfUsrID > 0 {
dfusrID = entry.DfUsrID
} else {
dfusrID = nil
}
// ---------- USERNAME ----------
var username any
if entry.Username != "" {
username = entry.Username
} else {
username = nil
}
// ---------- ROLE CODE (SNAPSHOT) ----------
roleCode := entry.RoleCode
if roleCode == "" {
roleCode = "public"
}
// ---------- TARGET ----------
var targetDfUsrID any
if entry.TargetDfUsrID > 0 {
targetDfUsrID = entry.TargetDfUsrID
} else {
targetDfUsrID = nil
}
targetUsername := nullIfEmpty(entry.TargetUsername)
log.Printf(
"🧾 auditlog INSERT | actor_dfusr=%v actor_user=%v role=%s %s %s target=%v",
dfusrID,
username,
roleCode,
entry.ActionCategory,
entry.ActionTarget,
targetDfUsrID,
)
_, err := dbConn.Exec(`
INSERT INTO mk_user_activity_log (
log_id,
dfusr_id,
username,
role_code,
action_type,
action_category,
action_target,
description,
ip_address,
user_agent,
session_id,
request_started_at,
request_finished_at,
duration_ms,
http_status,
is_success,
error_message,
-- ✅ NEW
target_dfusr_id,
target_username,
change_before,
change_after,
created_at
) VALUES (
gen_random_uuid(),
$1,$2,$3,
$4,$5,$6,$7,
$8,$9,$10,
$11,$12,$13,$14,
$15,$16,
$17,$18,$19,$20,
now()
)
`,
dfusrID,
username,
roleCode,
entry.ActionType,
entry.ActionCategory,
entry.ActionTarget,
entry.Description,
entry.IpAddress,
entry.UserAgent,
entry.SessionID,
nullIfZeroTime(entry.RequestStartedAt),
nullIfZeroTime(entry.RequestFinishedAt),
nullIfZeroInt(entry.DurationMs),
nullIfZeroInt(entry.HttpStatus),
entry.IsSuccess,
entry.ErrorMessage,
// ✅ NEW
targetDfUsrID,
targetUsername,
toJSONB(entry.ChangeBefore),
toJSONB(entry.ChangeAfter),
)
if err != nil {
log.Println("❌ auditlog insert error:", err)
}
}
}

View File

@@ -0,0 +1,25 @@
package auditlog
import "context"
func Write(log ActivityLog) {
if logQueue == nil {
return // sistem henüz init edilmediyse sessizce çık
}
select {
case logQueue <- log:
// kuyruğa alındı
default:
// kuyruk dolu → drop edilir, ana akış bozulmaz
}
}
func Enqueue(ctx context.Context, al ActivityLog) {
select {
case logQueue <- al:
// ok
default:
// queue dolu → drop
}
}

View File

@@ -0,0 +1,36 @@
package authz
import "context"
type scopeKey string
const (
CtxDeptCodesKey scopeKey = "authz.dept_codes"
CtxPiyasaCodesKey scopeKey = "authz.piyasa_codes"
)
func WithDeptCodes(ctx context.Context, codes []string) context.Context {
return context.WithValue(ctx, CtxDeptCodesKey, codes)
}
func WithPiyasaCodes(ctx context.Context, codes []string) context.Context {
return context.WithValue(ctx, CtxPiyasaCodesKey, codes)
}
func GetDeptCodesFromCtx(ctx context.Context) []string {
if v := ctx.Value(CtxDeptCodesKey); v != nil {
if codes, ok := v.([]string); ok {
return codes
}
}
return nil
}
func GetPiyasaCodesFromCtx(ctx context.Context) []string {
if v := ctx.Value(CtxPiyasaCodesKey); v != nil {
if codes, ok := v.([]string); ok {
return codes
}
}
return nil
}

View File

@@ -0,0 +1,32 @@
package authz
import (
"context"
"fmt"
"strings"
)
func BuildMSSQLPiyasaFilter(
ctx context.Context,
column string,
) string {
codes := GetPiyasaCodesFromCtx(ctx)
if len(codes) == 0 {
return "1=1"
}
var quoted []string
for _, c := range codes {
quoted = append(quoted, "'"+c+"'")
}
return fmt.Sprintf(
"%s IN (%s)",
column,
strings.Join(quoted, ","),
)
}

View File

@@ -0,0 +1,24 @@
package authz
import (
"fmt"
"strings"
)
func BuildINClause(column string, codes []string) string {
if len(codes) == 0 {
return "1=0"
}
var quoted []string
for _, c := range codes {
c = strings.TrimSpace(strings.ToUpper(c))
if c == "" {
continue
}
quoted = append(quoted, "'"+c+"'")
}
if len(quoted) == 0 {
return "1=0"
}
return fmt.Sprintf("%s IN (%s)", column, strings.Join(quoted, ","))
}

View File

@@ -0,0 +1,74 @@
package authz
import (
"database/sql"
"fmt"
"sync"
)
// =====================================================
// 🧠 PIYASA CACHE (USER → CODES)
// =====================================================
var (
piyasaCache = make(map[int][]string)
piyasaMu sync.RWMutex
)
// =====================================================
// 📌 GET USER PIYASA CODES (CACHED)
// =====================================================
func GetUserPiyasaCodes(pg *sql.DB, userID int) ([]string, error) {
// -----------------------------
// CACHE READ
// -----------------------------
piyasaMu.RLock()
if it, ok := piyasaCache[userID]; ok {
piyasaMu.RUnlock()
return it, nil
}
piyasaMu.RUnlock()
// -----------------------------
// DB QUERY
// -----------------------------
rows, err := pg.Query(`
SELECT piyasa_code
FROM dfusr_piyasa
WHERE dfusr_id = $1
AND is_allowed = true
`, userID)
if err != nil {
return nil, fmt.Errorf("pg piyasa query error: %w", err)
}
defer rows.Close()
var out []string
for rows.Next() {
var code string
if err := rows.Scan(&code); err == nil {
out = append(out, code)
}
}
// -----------------------------
// CACHE WRITE
// -----------------------------
piyasaMu.Lock()
piyasaCache[userID] = out
piyasaMu.Unlock()
return out, nil
}
// =====================================================
// 🧹 CLEAR USER PIYASA CACHE
// =====================================================
func ClearPiyasaCache(userID int) {
piyasaMu.Lock()
defer piyasaMu.Unlock()
delete(piyasaCache, userID)
}

View File

@@ -0,0 +1,50 @@
package mailer
import (
"os"
"strconv"
"strings"
)
type Config struct {
Host string
Port int
Username string
Password string
From string
StartTLS bool
}
func ConfigFromEnv() (Config, error) {
var cfg Config
cfg.Host = strings.TrimSpace(os.Getenv("SMTP_HOST"))
cfg.Username = strings.TrimSpace(os.Getenv("SMTP_USERNAME"))
cfg.Password = os.Getenv("SMTP_PASSWORD")
cfg.From = strings.TrimSpace(os.Getenv("SMTP_FROM"))
portStr := strings.TrimSpace(os.Getenv("SMTP_PORT"))
if portStr == "" {
cfg.Port = 587
} else {
p, err := strconv.Atoi(portStr)
if err != nil {
return Config{}, err
}
cfg.Port = p
}
startTLS := strings.TrimSpace(os.Getenv("SMTP_STARTTLS"))
if startTLS == "" {
cfg.StartTLS = true
} else {
cfg.StartTLS = strings.EqualFold(startTLS, "true") || startTLS == "1"
}
// minimal validation
if cfg.Host == "" || cfg.Username == "" || cfg.Password == "" || cfg.From == "" || cfg.Port <= 0 {
return Config{}, ErrInvalidConfig
}
return cfg, nil
}

View File

@@ -0,0 +1,270 @@
package mailer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"golang.org/x/oauth2/clientcredentials"
)
type GraphMailer struct {
httpClient *http.Client
from string // noreply@baggi.com.tr
replyTo string // opsiyonel
}
func NewGraphMailer() (*GraphMailer, error) {
tenantID := strings.TrimSpace(os.Getenv("AZURE_TENANT_ID"))
clientID := strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID"))
clientSecret := strings.TrimSpace(os.Getenv("AZURE_CLIENT_SECRET"))
from := strings.TrimSpace(os.Getenv("MAIL_FROM"))
replyTo := strings.TrimSpace(os.Getenv("MAIL_REPLY_TO")) // opsiyonel
if tenantID == "" || clientID == "" || clientSecret == "" || from == "" {
return nil, fmt.Errorf("azure graph mailer env missing (AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET/MAIL_FROM)")
}
conf := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID),
Scopes: []string{"https://graph.microsoft.com/.default"},
}
httpClient := conf.Client(context.Background())
httpClient.Timeout = 25 * time.Second
log.Printf("✉️ Graph Mailer hazır (App-only token) | from=%s", from)
return &GraphMailer{
httpClient: httpClient,
from: from,
replyTo: replyTo,
}, nil
}
func (g *GraphMailer) Send(ctx context.Context, msg Message) error {
start := time.Now()
// ---------- validate ----------
cleanTo := make([]string, 0, len(msg.To))
for _, t := range msg.To {
t = strings.TrimSpace(t)
if t != "" {
cleanTo = append(cleanTo, t)
}
}
if len(cleanTo) == 0 {
return fmt.Errorf("recipient missing")
}
subject := strings.TrimSpace(msg.Subject)
if subject == "" {
return fmt.Errorf("subject missing")
}
// internal-safe adjustments
isInternal := allRecipientsInDomain(cleanTo, "baggi.com.tr")
if isInternal && !strings.HasPrefix(subject, "[BSSApp]") {
subject = "[BSSApp] " + subject
}
html := strings.TrimSpace(msg.BodyHTML)
text := strings.TrimSpace(msg.Body)
// Eğer sadece HTML geldiyse text üret (spam skorunu düşürür)
if text == "" && html != "" {
text = stripHTMLVerySimple(html)
}
// Eğer sadece text geldiyse basit HTML üret
if html == "" && text != "" {
html = "<pre style=\"font-family:Segoe UI, Arial, sans-serif; white-space:pre-wrap;\">" +
htmlEscape(text) + "</pre>"
}
if html == "" {
return fmt.Errorf("body missing (Body or BodyHTML)")
}
log.Printf("✉️ [MAIL] SEND START | from=%s | to=%v | internal=%v | subject=%s", g.from, cleanTo, isInternal, subject)
// ---------- build recipients ----------
type recipient struct {
EmailAddress struct {
Address string `json:"address"`
} `json:"emailAddress"`
}
toRecipients := make([]recipient, 0, len(cleanTo))
for _, m := range cleanTo {
r := recipient{}
r.EmailAddress.Address = m
toRecipients = append(toRecipients, r)
}
// ---------- headers to reduce auto-phish/spam signals ----------
headers := []map[string]string{
{
"name": "X-Mailer",
"value": "BSSApp Graph Mailer",
},
{
"name": "X-BSSApp-Internal",
"value": strconv.FormatBool(isInternal),
},
}
// replyTo (opsiyonel)
var replyToRecipients []recipient
if strings.TrimSpace(g.replyTo) != "" {
rt := recipient{}
rt.EmailAddress.Address = strings.TrimSpace(g.replyTo)
replyToRecipients = []recipient{rt}
}
// ---------- Graph payload ----------
message := map[string]any{
"subject": subject,
"body": map[string]string{
"contentType": "HTML",
"content": buildHTML(html, text, isInternal),
},
"toRecipients": toRecipients,
"internetMessageHeaders": headers,
"importance": "normal",
}
// replyTo SADECE doluysa ekle
if len(replyToRecipients) > 0 {
message["replyTo"] = replyToRecipients
}
payload := map[string]any{
"message": message,
"saveToSentItems": true,
}
b, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("json marshal: %w", err)
}
url := fmt.Sprintf(
"https://graph.microsoft.com/v1.0/users/%s/sendMail",
g.from,
)
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
bytes.NewBuffer(b),
)
if err != nil {
return fmt.Errorf("new request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
res, err := g.httpClient.Do(req)
if err != nil {
return fmt.Errorf("graph request: %w", err)
}
defer res.Body.Close()
if res.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(res.Body)
log.Printf(
"❌ [MAIL] SEND FAILED | status=%s | body=%s",
res.Status,
string(bodyBytes),
)
return fmt.Errorf("graph send mail failed: %s", res.Status)
}
log.Printf(
"✅ [MAIL] SEND OK | to=%v | duration=%s",
cleanTo,
time.Since(start),
)
return nil
}
// ---------- helpers ----------
func allRecipientsInDomain(to []string, domain string) bool {
domain = strings.ToLower(strings.TrimSpace(domain))
for _, addr := range to {
addr = strings.ToLower(strings.TrimSpace(addr))
if !strings.HasSuffix(addr, "@"+domain) {
return false
}
}
return true
}
func buildHTML(htmlBody, textBody string, internal bool) string {
// Internal ise daha sade, daha az “marketing-like”
if internal {
return `
<div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#1f1f1f;">
` + htmlBody + `
<hr style="border:none;border-top:1px solid #e5e5e5;margin:16px 0;" />
<div style="font-size:12px;color:#666;">
Bu e-posta BSSApp sistemi tarafından otomatik oluşturulmuştur.
</div>
</div>`
}
// External
return `
<div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#1f1f1f;">
` + htmlBody + `
</div>`
}
// Çok basit “html -> text” (tam değil ama yeterli)
func stripHTMLVerySimple(s string) string {
s = strings.ReplaceAll(s, "<br>", "\n")
s = strings.ReplaceAll(s, "<br/>", "\n")
s = strings.ReplaceAll(s, "<br />", "\n")
s = strings.ReplaceAll(s, "</p>", "\n\n")
// kaba temizlik
for {
i := strings.Index(s, "<")
j := strings.Index(s, ">")
if i >= 0 && j > i {
s = s[:i] + s[j+1:]
continue
}
break
}
return strings.TrimSpace(s)
}
func htmlEscape(s string) string {
replacer := strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
"\"", "&quot;",
"'", "&#39;",
)
return replacer.Replace(s)
}
func (g *GraphMailer) SendMail(to string, subject string, html string) error {
msg := Message{
To: []string{to},
Subject: subject,
BodyHTML: html,
}
// context burada internal olarak veriliyor
return g.Send(context.Background(), msg)
}

View File

@@ -0,0 +1,148 @@
package mailer
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/smtp"
"strings"
"time"
)
var ErrInvalidConfig = errors.New("invalid smtp config")
type Mailer struct {
cfg Config
}
type Message struct {
To []string
Subject string
Body string
BodyHTML string
}
func New(cfg Config) *Mailer {
return &Mailer{cfg: cfg}
}
func (m *Mailer) Send(ctx context.Context, msg Message) error {
if len(msg.To) == 0 {
return errors.New("recipient missing")
}
if strings.TrimSpace(msg.Subject) == "" {
return errors.New("subject missing")
}
// timeout kontrolü (ctx)
deadline, ok := ctx.Deadline()
if !ok {
// default timeout
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 20*time.Second)
defer cancel()
deadline, _ = ctx.Deadline()
}
timeout := time.Until(deadline)
if timeout <= 0 {
return context.DeadlineExceeded
}
addr := fmt.Sprintf("%s:%d", m.cfg.Host, m.cfg.Port)
dialer := net.Dialer{Timeout: timeout}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("smtp dial: %w", err)
}
defer conn.Close()
c, err := smtp.NewClient(conn, m.cfg.Host)
if err != nil {
return fmt.Errorf("smtp client: %w", err)
}
defer c.Close()
// STARTTLS
if m.cfg.StartTLS {
tlsCfg := &tls.Config{
ServerName: m.cfg.Host,
MinVersion: tls.VersionTLS12,
}
if err := c.StartTLS(tlsCfg); err != nil {
return fmt.Errorf("starttls: %w", err)
}
}
// AUTH
auth := smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
if err := c.Auth(auth); err != nil {
return fmt.Errorf("smtp auth: %w", err)
}
// MAIL FROM
if err := c.Mail(m.cfg.From); err != nil {
return fmt.Errorf("mail from: %w", err)
}
// RCPT TO
for _, rcpt := range msg.To {
rcpt = strings.TrimSpace(rcpt)
if rcpt == "" {
continue
}
if err := c.Rcpt(rcpt); err != nil {
return fmt.Errorf("rcpt %s: %w", rcpt, err)
}
}
w, err := c.Data()
if err != nil {
return fmt.Errorf("data: %w", err)
}
// basit mime
contentType := "text/plain; charset=UTF-8"
body := msg.Body
if strings.TrimSpace(msg.BodyHTML) != "" {
contentType = "text/html; charset=UTF-8"
body = msg.BodyHTML
}
raw := buildMIME(m.cfg.From, msg.To, msg.Subject, contentType, body)
_, writeErr := w.Write([]byte(raw))
closeErr := w.Close()
if writeErr != nil {
return fmt.Errorf("write body: %w", writeErr)
}
if closeErr != nil {
return fmt.Errorf("close body: %w", closeErr)
}
if err := c.Quit(); err != nil {
return fmt.Errorf("quit: %w", err)
}
return nil
}
func buildMIME(from string, to []string, subject, contentType, body string) string {
// Subject UTF-8 basit hali (gerekirse sonra MIME encoded-word ekleriz)
headers := []string{
"From: " + from,
"To: " + strings.Join(to, ", "),
"Subject: " + subject,
"MIME-Version: 1.0",
"Content-Type: " + contentType,
"",
}
return strings.Join(headers, "\r\n") + "\r\n" + body + "\r\n"
}
type MailerInterface interface {
Send(ctx context.Context, msg Message) error
}

View File

@@ -0,0 +1,18 @@
package mailer
import "fmt"
func (m *GraphMailer) SendPasswordResetMail(toEmail string, resetURL string) error {
subject := "Parola Sıfırlama"
html := fmt.Sprintf(`
<p>Merhaba,</p>
<p>Parolanızı sıfırlamak için aşağıdaki bağlantıya tıklayın:</p>
<p>
<a href="%s">%s</a>
</p>
<p>Bu bağlantı <strong>30 dakika</strong> geçerlidir ve tek kullanımlıktır.</p>
`, resetURL, resetURL)
return m.SendMail(toEmail, subject, html)
}

View File

@@ -0,0 +1,11 @@
package security
import "errors"
var (
ErrPasswordTooShort = errors.New("password must be at least 8 characters")
ErrPasswordUpper = errors.New("password must contain an uppercase letter")
ErrPasswordLower = errors.New("password must contain a lowercase letter")
ErrPasswordDigit = errors.New("password must contain a digit")
ErrPasswordSpecial = errors.New("password must contain a special character")
)

View File

@@ -0,0 +1,35 @@
package security
import (
"errors"
"regexp"
"strings"
)
var (
reUpper = regexp.MustCompile(`[A-Z]`)
reLower = regexp.MustCompile(`[a-z]`)
reDigit = regexp.MustCompile(`[0-9]`)
reSpecial = regexp.MustCompile(`[^A-Za-z0-9]`)
)
func ValidatePassword(pw string) error {
pw = strings.TrimSpace(pw)
if len(pw) < 8 {
return errors.New("Parola en az 8 karakter olmalı")
}
if !reUpper.MatchString(pw) {
return errors.New("Parola en az 1 büyük harf içermeli")
}
if !reLower.MatchString(pw) {
return errors.New("Parola en az 1 küçük harf içermeli")
}
if !reDigit.MatchString(pw) {
return errors.New("Parola en az 1 rakam içermeli")
}
if !reSpecial.MatchString(pw) {
return errors.New("Parola en az 1 özel karakter içermeli")
}
return nil
}

View File

@@ -0,0 +1,13 @@
package security
import (
"os"
)
func BuildResetURL(token string) string {
base := os.Getenv("FRONTEND_URL")
if base == "" {
base = "http://localhost:9000"
}
return base + "/password-reset/" + token
}

View File

@@ -0,0 +1,23 @@
package security
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
)
func GenerateRefreshToken() (plain string, hash string, err error) {
b := make([]byte, 32) // 256 bit
if _, err = rand.Read(b); err != nil {
return
}
plain = hex.EncodeToString(b)
sum := sha256.Sum256([]byte(plain))
hash = hex.EncodeToString(sum[:])
return
}
func HashRefreshToken(plain string) string {
sum := sha256.Sum256([]byte(plain))
return hex.EncodeToString(sum[:])
}

View File

@@ -0,0 +1,26 @@
package security
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
)
func GenerateResetToken() (plain string, hash string, err error) {
b := make([]byte, 32) // 256 bit
if _, err = rand.Read(b); err != nil {
return
}
plain = hex.EncodeToString(b)
sum := sha256.Sum256([]byte(plain))
hash = hex.EncodeToString(sum[:])
return
}
func HashToken(plain string) string {
sum := sha256.Sum256([]byte(plain))
return hex.EncodeToString(sum[:])
}

4
svc/mail.env Normal file
View File

@@ -0,0 +1,4 @@
AZURE_TENANT_ID=c8e0675d-1f6e-40f3-ba5f-3d1985b92317
AZURE_CLIENT_ID=94a134b7-757f-4bcc-9e4b-d577b631a9a3
AZURE_CLIENT_SECRET=PaW8Q~9NzYXHrESZcKoP6.hRxS.CyQshvJ0Y0czx
MAIL_FROM=baggiss@baggi.com.tr

587
svc/main.go Normal file
View File

@@ -0,0 +1,587 @@
package main
import (
"bssapp-backend/db"
"bssapp-backend/internal/auditlog"
"bssapp-backend/internal/mailer"
"bssapp-backend/middlewares"
"bssapp-backend/permissions"
"bssapp-backend/repository"
"bssapp-backend/routes"
"database/sql"
"log"
"net/http"
"os"
"runtime/debug"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
/*
===========================================================
✅ CORS
===========================================================
*/
func enableCORS(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:9000")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
h.ServeHTTP(w, r)
})
}
/*
===========================================================
✅ V3 — Method-aware Route Auto Register
- mk_sys_routes: (path, method, module_code, action)
- unique: (path, method)
- admin auto-allow: mk_sys_role_permissions (role_id=3)
===========================================================
*/
func autoRegisterRouteV3(
pg *sql.DB,
path string,
method string,
module string,
action string,
) {
tx, err := pg.Begin()
if err != nil {
log.Println("❌ TX begin error:", err)
return
}
defer tx.Rollback()
// 1) ROUTE REGISTER (path+method)
_, err = tx.Exec(`
INSERT INTO mk_sys_routes
(path, method, module_code, action)
VALUES
($1, $2, $3, $4)
ON CONFLICT (path, method) DO UPDATE
SET
module_code = EXCLUDED.module_code,
action = EXCLUDED.action
`,
path,
method,
module,
action,
)
if err != nil {
log.Printf("❌ Route register error (%s %s): %v", method, path, err)
return
}
// 2) ADMIN AUTO PERMISSION (module+action bazlı)
_, err = tx.Exec(`
INSERT INTO mk_sys_role_permissions
(role_id, module_code, action, allowed)
SELECT
id,
$1,
$2,
true
FROM dfrole
WHERE id = 3 -- ADMIN
ON CONFLICT DO NOTHING
`,
module,
action,
)
if err != nil {
log.Printf("❌ Admin perm seed error (%s %s): %v", method, path, err)
return
}
if err := tx.Commit(); err != nil {
log.Println("❌ TX commit error:", err)
return
}
log.Printf("✅ Route+Perm registered → %s %s [%s:%s]",
method, path, module, action,
)
}
/*
===========================================================
✅ V3 Route Bind Helper
- tek satırda: autoRegister + wrap + Handle
===========================================================
*/
func bindV3(
r *mux.Router,
pg *sql.DB,
path string,
method string,
module string,
action string,
h http.Handler,
) {
// main
autoRegisterRouteV3(pg, path, method, module, action)
r.Handle(path, h).Methods(method, "OPTIONS")
}
/*
===========================================================
InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION
===========================================================
*/
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
r := mux.NewRouter()
/*
===========================================================
✅ wrapV3 (method-aware):
- AuthMiddleware (JWT)
- panic recover
- AuthzGuardByRoute(pgDB) => route(path+method) lookup
===========================================================
*/
wrapV3 := func(h http.Handler) http.Handler {
return middlewares.AuthMiddleware(
pgDB,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("🔥 PANIC %s %s\n%v", r.Method, r.URL.Path, rec)
debug.PrintStack()
http.Error(w, "internal server error", 500)
}
}()
// ✅ method-aware route guard
middlewares.AuthzGuardByRoute(pgDB)(h).ServeHTTP(w, r)
}),
)
}
// ============================================================
// PUBLIC (NO AUTHZ)
// ============================================================
bindV3(r, pgDB,
"/api/auth/login", "POST",
"auth", "login",
http.HandlerFunc(routes.LoginHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/auth/refresh", "POST",
"auth", "refresh",
routes.AuthRefreshHandler(pgDB),
)
// ============================================================
// SYSTEM
// ============================================================
bindV3(r, pgDB,
"/api/password/change", "POST",
"system", "update",
wrapV3(http.HandlerFunc(routes.FirstPasswordChangeHandler(pgDB))),
)
bindV3(r, pgDB,
"/api/activity-logs", "GET",
"user", "view",
wrapV3(routes.AdminActivityLogsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/test-mail", "POST",
"user", "insert",
wrapV3(routes.TestMailHandler(ml)),
)
// ============================================================
// PERMISSIONS
// ============================================================
rolePerm := "/api/roles/{id}/permissions"
bindV3(r, pgDB,
rolePerm, "GET",
"user", "update",
wrapV3(routes.GetRolePermissionMatrix(pgDB)),
)
bindV3(r, pgDB,
rolePerm, "POST",
"user", "update",
wrapV3(routes.SaveRolePermissionMatrix(pgDB)),
)
userPerm := "/api/users/{id}/permissions"
bindV3(r, pgDB,
userPerm, "GET",
"user", "update",
wrapV3(routes.GetUserPermissionsHandler(pgDB)),
)
bindV3(r, pgDB,
userPerm, "POST",
"user", "update",
wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
)
// ✅ permissions/routes (system:view)
bindV3(r, pgDB,
"/api/permissions/routes", "GET",
"system", "view",
wrapV3(routes.GetUserRoutePermissionsHandler(pgDB)),
)
// ✅ permissions/effective (system:view)
bindV3(r, pgDB,
"/api/permissions/effective", "GET",
"system", "view",
wrapV3(routes.GetMyEffectivePermissions(pgDB)),
)
// ✅ my permission matrix (system:view) (Senin tabloda user:view görünüyor; düzeltiyoruz)
bindV3(r, pgDB,
"/api/permissions/matrix", "GET",
"system", "view",
wrapV3(routes.GetMyPermissionMatrix(pgDB)),
)
// ============================================================
// ROLE + DEPARTMENT PERMISSIONS
// ============================================================
rdPerm := "/api/roles/{roleId}/departments/{deptCode}/permissions"
rdHandler := routes.NewRoleDepartmentPermissionHandler(pgDB)
bindV3(r, pgDB,
rdPerm, "GET",
"user", "update",
wrapV3(http.HandlerFunc(rdHandler.Get)),
)
bindV3(r, pgDB,
rdPerm, "POST",
"user", "update",
wrapV3(http.HandlerFunc(rdHandler.Save)),
)
// ============================================================
// USERS
// ============================================================
bindV3(r, pgDB,
"/api/users/list", "GET",
"user", "view",
wrapV3(routes.UserListRoute(pgDB)),
)
bindV3(r, pgDB,
"/api/users", "POST",
"user", "insert",
wrapV3(routes.UserCreateRoute(pgDB)),
)
bindV3(r, pgDB,
"/api/users/{id}", "GET",
"user", "update",
wrapV3(routes.UserDetailRoute(pgDB)),
)
bindV3(r, pgDB,
"/api/users/{id}", "PUT",
"user", "update",
wrapV3(routes.UserDetailRoute(pgDB)),
)
bindV3(r, pgDB,
"/api/users/{id}/admin-reset-password", "POST",
"user", "update",
wrapV3(routes.AdminResetPasswordHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/users/{id}/send-password-mail", "POST",
"user", "update",
wrapV3(routes.SendPasswordResetMailHandler(pgDB, ml)),
)
// ✅ eski kısayol create endpoint (senin eski main.goda vardı)
bindV3(r, pgDB,
"/api/users/create", "POST",
"user", "insert",
wrapV3(routes.UserCreateRoute(pgDB)),
)
// ============================================================
// LOOKUPS
// ============================================================
lookups := map[string]http.Handler{
"/api/lookups/roles": routes.GetRoleLookupRoute(pgDB),
"/api/lookups/departments": routes.GetDepartmentLookupRoute(pgDB),
"/api/lookups/nebim-users": routes.GetNebimUserLookupRoute(pgDB),
"/api/lookups/piyasalar": routes.GetPiyasaLookupRoute(pgDB),
"/api/lookups/users-perm": routes.GetUsersForPermissionSelectRoute(pgDB),
"/api/lookups/roles-perm": routes.GetRolesForPermissionSelectRoute(pgDB),
"/api/lookups/departments-perm": routes.GetDepartmentsForPermissionSelectRoute(pgDB),
"/api/lookups/modules": routes.GetModuleLookupRoute(pgDB),
}
for path, handler := range lookups {
bindV3(r, pgDB,
path, "GET",
"user", "view",
wrapV3(handler),
)
}
// ============================================================
// CUSTOMER
// ============================================================
bindV3(r, pgDB,
"/api/accounts", "GET",
"customer", "view",
wrapV3(http.HandlerFunc(routes.GetAccountsHandler)),
)
bindV3(r, pgDB,
"/api/customer-list", "GET",
"customer", "view",
wrapV3(http.HandlerFunc(routes.GetCustomerListHandler)),
)
// ============================================================
// FINANCE
// ============================================================
bindV3(r, pgDB,
"/api/today-currency", "GET",
"finance", "view",
wrapV3(routes.GetTodayCurrencyV3Handler(mssql)),
)
bindV3(r, pgDB,
"/api/export-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportPDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/exportstamentheaderreport-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportStatementHeaderReportPDFHandler(mssql)),
)
// ============================================================
// REPORT (STATEMENTS)
// ============================================================
bindV3(r, pgDB,
"/api/statements", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetStatementHeadersHandler)),
)
// ⚠️ Senin handler: GetStatementDetailsHandler vars["accountCode"] bekliyor,
// routeda {id} var. Burada, DB routeunu ve pathi bozmayalım diye {id}yi koruyorum,
// ama handler içinde accountCode := mux.Vars(r)["id"] yapman daha doğru.
bindV3(r, pgDB,
"/api/statements/{id}/details", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetStatementDetailsHandler)),
)
// ============================================================
// ORDER
// ============================================================
orderRoutes := []struct {
Path string
Method string
Action string
Handle http.Handler
}{
{"/api/order/create", "POST", "insert", routes.CreateOrderHandler(pgDB, mssql)},
{"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)},
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
{"/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-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)},
{"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},
{"/api/min-price", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},
}
for _, rt := range orderRoutes {
bindV3(r, pgDB,
rt.Path, rt.Method,
"order", rt.Action,
wrapV3(rt.Handle),
)
}
// ============================================================
// PRODUCTS (✅ handler mapping fix)
// ============================================================
bindV3(r, pgDB,
"/api/products", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductListHandler)),
)
bindV3(r, pgDB,
"/api/product-detail", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductDetailHandler)),
)
bindV3(r, pgDB,
"/api/product-colors", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductColorsHandler)),
)
bindV3(r, pgDB,
"/api/product-colorsize", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductColorSizesHandler)),
)
bindV3(r, pgDB,
"/api/product-secondcolor", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)),
)
// ============================================================
// ROLE MANAGEMENT
// ============================================================
bindV3(r, pgDB,
"/api/roles", "GET",
"user", "view",
wrapV3(routes.GetRolesHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/departments", "GET",
"user", "view",
wrapV3(routes.GetDepartmentsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/piyasalar", "GET",
"user", "view",
wrapV3(routes.GetPiyasalarHandler(pgDB)),
)
// ============================================================
// ROLE RELATIONS
// ============================================================
bindV3(r, pgDB,
"/api/roles/{id}/departments", "POST",
"user", "update",
wrapV3(routes.UpdateRoleDepartmentsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/roles/{id}/piyasalar", "POST",
"user", "update",
wrapV3(routes.UpdateRolePiyasalarHandler(pgDB)),
)
// ============================================================
// USER ↔ ROLE
// ============================================================
bindV3(r, pgDB,
"/api/users/{id}/roles", "POST",
"user", "update",
wrapV3(routes.UpdateUserRolesHandler(pgDB)),
)
// ============================================================
// ADMIN EXTRA (eski main.goda vardı, yeni sisteme alındı)
// ============================================================
bindV3(r, pgDB,
"/api/admin/users/{id}/piyasa-sync", "POST",
"admin", "user.update",
wrapV3(http.HandlerFunc(routes.AdminSyncUserPiyasaHandler)),
)
return r
}
func main() {
log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥")
// -------------------------------------------------------
// 🔑 ENV
// -------------------------------------------------------
if err := godotenv.Load(".env", "mail.env"); err != nil {
log.Println("⚠️ .env / mail.env bulunamadı")
}
jwtSecret := os.Getenv("JWT_SECRET")
if len(jwtSecret) < 10 {
log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)")
}
log.Println("🔐 JWT_SECRET yüklendi")
// -------------------------------------------------------
// 🔗 DATABASE
// -------------------------------------------------------
db.ConnectMSSQL()
pgDB, err := db.ConnectPostgres()
if err != nil {
log.Fatalf("❌ PostgreSQL bağlantı hatası: %v", err)
}
defer pgDB.Close()
// -------------------------------------------------------
// 🔐 ADMIN ROLE + DEPARTMENT AUTO SEED
// -------------------------------------------------------
if err := permissions.SeedAdminRoleDepartments(pgDB); err != nil {
log.Println("❌ Admin dept seed failed:", err)
} else {
log.Println("✅ Admin dept permissions seeded")
}
// -------------------------------------------------------
// 🕵️ AUDIT LOG INIT
// -------------------------------------------------------
auditlog.Init(pgDB, 1000)
log.Println("🕵️ AuditLog sistemi başlatıldı (buffer=1000)")
// -------------------------------------------------------
// ✉️ MAILER INIT
// -------------------------------------------------------
graphMailer, err := mailer.NewGraphMailer()
if err != nil {
log.Fatalf("❌ Graph Mailer init error: %v", err)
}
log.Println("✉️ Graph Mailer hazır")
// -------------------------------------------------------
// 👤 DEBUG
// -------------------------------------------------------
repository.NewUserRepository(pgDB).DebugListUsers()
// -------------------------------------------------------
// 🌍 SERVER
// -------------------------------------------------------
router := InitRoutes(pgDB, db.MssqlDB, graphMailer)
handler := enableCORS(
middlewares.GlobalAuthMiddleware(
pgDB,
middlewares.RequestLogger(router),
),
)
log.Println("✅ Server çalışıyor: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}

View File

@@ -0,0 +1,71 @@
package middlewares
import (
"bssapp-backend/auth"
"net/http"
"time"
"bssapp-backend/internal/auditlog"
)
type ResponseWriter struct {
http.ResponseWriter
status int
}
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
return &ResponseWriter{
ResponseWriter: w,
status: http.StatusOK,
}
}
func (rw *ResponseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *ResponseWriter) Status() int { return rw.status }
func Audit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := NewResponseWriter(w)
next.ServeHTTP(rw, r)
// ✅ AuthMiddleware sonrası burada claims VAR
var dfusrID int64
var username, roleCode string
if claims, ok := auth.GetClaimsFromContext(r.Context()); ok && claims != nil {
dfusrID = int64(claims.ID)
username = claims.Username
roleCode = claims.RoleCode // tokenda varsa
}
entry := auditlog.ActivityLog{
DfUsrID: dfusrID,
Username: username,
RoleCode: roleCode,
ActionType: "route_access",
ActionCategory: "nav",
ActionTarget: r.URL.Path,
Description: r.Method + " " + r.URL.Path,
IpAddress: r.RemoteAddr,
UserAgent: r.UserAgent(),
SessionID: "",
RequestStartedAt: start,
RequestFinishedAt: time.Now(),
DurationMs: int(time.Since(start).Milliseconds()),
HttpStatus: rw.Status(),
IsSuccess: rw.Status() < 400,
}
auditlog.Write(entry)
})
}

View File

@@ -0,0 +1,39 @@
package middlewares
import (
"bssapp-backend/auth"
"database/sql"
"log"
"net/http"
"strings"
)
func AuthMiddleware(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
claims, err := auth.ValidateToken(parts[1])
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 🔥 BU SATIR ŞART
ctx := auth.WithClaims(r.Context(), claims)
log.Printf("🔐 AUTH CTX SET user=%d role=%s", claims.ID, claims.RoleCode)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

961
svc/middlewares/authz_v2.go Normal file
View File

@@ -0,0 +1,961 @@
package middlewares
import (
"bssapp-backend/internal/authz"
"bssapp-backend/permissions"
"bytes"
"database/sql"
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"bssapp-backend/auth"
"github.com/gorilla/mux"
)
/*
AuthzGuardV2
- module+action role permission check (mk_sys_role_permissions)
- optional scope checks (department / piyasa) via intersection:
user_allowed ∩ role_allowed
- cache with TTL
Expected:
- AuthMiddleware runs before this and sets JWT claims in context.
- claims should contain RoleID and UserID.
*/
// =====================================================
// 🔧 CONFIG / CONSTANTS
// =====================================================
const (
defaultPermTTL = 60 * time.Second
defaultScopeTTL = 30 * time.Second
maxBodyRead = 1 << 20 // 1MB
)
// =====================================================
// 🧠 CACHE
// =====================================================
type cacheItem struct {
val any
expires time.Time
}
type ttlCache struct {
mu sync.RWMutex
ttl time.Duration
m map[string]cacheItem
}
// =====================================================
// 🌍 GLOBAL SCOPE CACHE (for invalidation)
// =====================================================
var globalScopeCache *ttlCache
func newTTLCache(ttl time.Duration) *ttlCache {
return &ttlCache{
ttl: ttl,
m: make(map[string]cacheItem),
}
}
func (c *ttlCache) get(key string) (any, bool) {
now := time.Now()
c.mu.RLock()
item, ok := c.m[key]
c.mu.RUnlock()
if !ok {
return nil, false
}
if now.After(item.expires) {
// lazy delete
c.mu.Lock()
delete(c.m, key)
c.mu.Unlock()
return nil, false
}
return item.val, true
}
func (c *ttlCache) set(key string, val any) {
c.mu.Lock()
c.m[key] = cacheItem{val: val, expires: time.Now().Add(c.ttl)}
c.mu.Unlock()
}
// =====================================================
// ✅ MAIN MIDDLEWARE
// =====================================================
type AuthzV2Options struct {
// If true, scope checks are attempted when scope can be extracted.
EnableScopeChecks bool
// If true, when scope is required but cannot be extracted, deny.
// If false, when scope cannot be extracted, scope check is skipped.
StrictScope bool
// Override TTLs (optional)
PermTTL time.Duration
ScopeTTL time.Duration
// Custom extractors (optional). If nil, built-in extractors are used.
ExtractDepartmentCodes func(r *http.Request) []string
ExtractPiyasaCodes func(r *http.Request) []string
// Decide whether this request should be treated as scope-sensitive.
// If nil, built-in heuristic is used.
IsScopeSensitive func(module string, r *http.Request) bool
}
func AuthzGuardV2(pg *sql.DB, module string, action string) func(http.Handler) http.Handler {
return AuthzGuardV2WithOptions(pg, module, action, AuthzV2Options{
EnableScopeChecks: true,
StrictScope: false,
})
}
func AuthzGuardV2WithOptions(pg *sql.DB, module string, action string, opt AuthzV2Options) func(http.Handler) http.Handler {
permTTL := opt.PermTTL
if permTTL <= 0 {
permTTL = defaultPermTTL
}
scopeTTL := opt.ScopeTTL
if scopeTTL <= 0 {
scopeTTL = defaultScopeTTL
}
permCache := newTTLCache(permTTL)
if globalScopeCache == nil {
globalScopeCache = newTTLCache(scopeTTL)
}
scopeCache := globalScopeCache
// default extractors
extractDept := opt.ExtractDepartmentCodes
if extractDept == nil {
extractDept = defaultExtractDepartmentCodes
}
extractPiy := opt.ExtractPiyasaCodes
if extractPiy == nil {
extractPiy = defaultExtractPiyasaCodes
}
isScopeSensitive := opt.IsScopeSensitive
if isScopeSensitive == nil {
isScopeSensitive = defaultIsScopeSensitive
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// OPTIONS passthrough
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
userID := claims.ID
roleCode := claims.RoleCode
// ADMIN BYPASS
if claims.IsAdmin() {
next.ServeHTTP(w, r)
return
}
// resolve role_id from role_code
roleID, err := cachedRoleID(pg, permCache, roleCode)
if err != nil {
log.Println("❌ role resolve error:", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// --------------------------------------------------
// 🔐 PERMISSION RESOLUTION (USER > ROLE > DENY)
// --------------------------------------------------
permRepo := permissions.NewPermissionRepository(pg)
allowed := false
resolved := false // karar verildi mi?
// --------------------------------------------------
// 1⃣ USER OVERRIDE (ÖNCELİK)
// --------------------------------------------------
overrides, err := permRepo.GetUserOverrides(userID)
if err != nil {
log.Println("❌ override load error:", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
for _, o := range overrides {
if o.Module == module &&
o.Action == action {
log.Printf(
"🔁 USER OVERRIDE → %s:%s = %v",
module,
action,
o.Allowed,
)
allowed = o.Allowed
resolved = true
break
}
}
// --------------------------------------------------
// 2⃣ ROLE + DEPARTMENT (NEW SYSTEM)
// --------------------------------------------------
if !resolved {
deptCodes := claims.DepartmentCodes
roleDeptAllowed, err := permRepo.ResolvePermissionChain(
userID,
roleID,
deptCodes,
module,
action,
)
if err != nil {
log.Println("❌ perm resolve error:", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if roleDeptAllowed {
log.Printf("🆕 ROLE+DEPT → %s:%s = true", module, action)
allowed = true
resolved = true
} else {
log.Printf("🆕 ROLE+DEPT → %s:%s = false (try legacy)", module, action)
}
}
// --------------------------------------------------
// 3⃣ ROLE ONLY (LEGACY FALLBACK)
// --------------------------------------------------
if !resolved {
legacyAllowed, err := cachedRolePermission(
pg,
permCache,
roleID,
module,
action,
)
if err != nil {
log.Println("❌ legacy perm error:", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
log.Printf("🕰️ LEGACY ROLE → %s:%s = %v", module, action, legacyAllowed)
allowed = legacyAllowed
resolved = true
}
// --------------------------------------------------
// 3⃣ FINAL DECISION
// --------------------------------------------------
if !allowed {
log.Printf(
"⛔ ACCESS DENIED user=%d %s:%s path=%s",
claims.ID,
module,
action,
r.URL.Path,
)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
log.Printf(
"✅ ACCESS OK user=%d %s:%s %s",
claims.ID,
module,
action,
r.URL.Path,
)
// --------------------------------------------------
// 4⃣ OPTIONAL SCOPE CHECKS (FINAL - SECURE)
// --------------------------------------------------
if opt.EnableScopeChecks && isScopeSensitive(module, r) {
// 🔹 Requestten gelenler
reqDepts := normalizeCodes(extractDept(r))
reqPiy := normalizeCodes(extractPiy(r))
ctx := r.Context()
// 🔹 USER PIYASA (DBden)
userPiy, err := authz.GetUserPiyasaCodes(pg, int(userID))
if err != nil {
log.Println("❌ piyasa load error:", err)
http.Error(w, "forbidden", 403)
return
}
userPiy = normalizeCodes(userPiy)
// ------------------------------------------------
// ✅ PIYASA INTERSECTION
// ------------------------------------------------
var effectivePiy []string
switch {
case len(reqPiy) > 0 && len(userPiy) > 0:
effectivePiy = intersect(reqPiy, userPiy)
case len(reqPiy) > 0 && len(userPiy) == 0:
// user piyasa tanımlı değilse request'e güvenme → boş kalsın (StrictScope varsa deny)
effectivePiy = nil
case len(reqPiy) == 0 && len(userPiy) > 0:
effectivePiy = userPiy
}
if len(reqPiy) > 0 && len(effectivePiy) == 0 {
// request piyasa istiyor ama user scope karşılamıyor
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// ------------------------------------------------
// ✅ CONTEXTE YAZ
// ------------------------------------------------
if len(reqDepts) > 0 {
ctx = authz.WithDeptCodes(ctx, reqDepts)
}
if len(effectivePiy) > 0 {
ctx = authz.WithPiyasaCodes(ctx, effectivePiy)
}
r = r.WithContext(ctx)
// ------------------------------------------------
// ❗ STRICT MODE
// ------------------------------------------------
if len(reqDepts) == 0 && len(effectivePiy) == 0 {
if opt.StrictScope {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
return
}
// ------------------------------------------------
// 🔍 DEPARTMENT CHECK
// ------------------------------------------------
if len(reqDepts) > 0 {
okDept, err := cachedDeptIntersectionAny(
pg,
scopeCache,
userID,
roleID,
reqDepts,
)
if err != nil {
log.Println("❌ dept scope error:", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if !okDept {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
}
// ------------------------------------------------
// 🔍 PIYASA CHECK
// ------------------------------------------------
if len(effectivePiy) > 0 {
okPiy, err := cachedPiyasaIntersectionAny(
pg,
scopeCache,
userID,
roleID,
effectivePiy,
)
if err != nil {
log.Println("❌ piyasa scope error:", err)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if !okPiy {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
}
}
// --------------------------------------------------
// ✅ ALLOW
// --------------------------------------------------
next.ServeHTTP(w, r)
})
}
}
// =====================================================
// 🔐 PERMISSION CHECK (mk_sys_role_permissions)
// =====================================================
func cachedRolePermission(pg *sql.DB, c *ttlCache, roleID int64, module, action string) (bool, error) {
key := "perm|" + itoa(roleID) + "|" + module + "|" + action
if v, ok := c.get(key); ok {
return v.(bool), nil
}
var allowed bool
err := pg.QueryRow(`
SELECT allowed
FROM mk_sys_role_permissions
WHERE role_id = $1 AND module_code = $2 AND action = $3
`, roleID, module, action).Scan(&allowed)
if err == sql.ErrNoRows {
c.set(key, false)
return false, nil
}
if err != nil {
return false, err
}
c.set(key, allowed)
return allowed, nil
}
// =====================================================
// 🧩 SCOPE INTERSECTION
// user scope ∩ role scope
// =====================================================
func cachedDeptIntersectionAny(pg *sql.DB, c *ttlCache, userID, roleID int64, deptCodes []string) (bool, error) {
// cache by exact request list (sorted would be ideal; normalizeCodes already stabilizes somewhat)
key := "deptAny|" + itoa(userID) + "|" + itoa(roleID) + "|" + strings.Join(deptCodes, ",")
if v, ok := c.get(key); ok {
return v.(bool), nil
}
// ANY match: if request wants multiple codes, allow if at least one is in intersection
// Intersection query:
// user departments: dfusr_dprt -> mk_dprt(code)
// role allowed: dfrole_dprt -> mk_dprt(id) OR directly by id
okAny := false
// We do it in a single query with ANY($3)
var dummy int
err := pg.QueryRow(`
SELECT 1
FROM dfusr_dprt ud
JOIN mk_dprt d ON d.id = ud.dprt_id
JOIN dfrole_dprt rd ON rd.dprt_id = ud.dprt_id
WHERE ud.dfusr_id = $1
AND rd.dfrole_id = $2
AND ud.is_active = true
AND rd.is_allowed = true
AND d.code = ANY($3)
LIMIT 1
`, userID, roleID, pqArray(deptCodes)).Scan(&dummy)
if err == sql.ErrNoRows {
c.set(key, false)
return false, nil
}
if err != nil {
return false, err
}
okAny = true
c.set(key, okAny)
return okAny, nil
}
func cachedPiyasaIntersectionAny(pg *sql.DB, c *ttlCache, userID, roleID int64, piyasaCodes []string) (bool, error) {
key := "piyAny|" + itoa(userID) + "|" + itoa(roleID) + "|" + strings.Join(piyasaCodes, ",")
if v, ok := c.get(key); ok {
return v.(bool), nil
}
var dummy int
err := pg.QueryRow(`
SELECT 1
FROM dfusr_piyasa up
WHERE up.dfusr_id = $1
AND up.is_allowed = true
AND up.piyasa_code = ANY($2)
LIMIT 1
`, userID, pqArray(piyasaCodes)).Scan(&dummy)
if err == sql.ErrNoRows {
c.set(key, false)
return false, nil
}
if err != nil {
return false, err
}
c.set(key, true)
return true, nil
}
// =====================================================
// 🧲 DEFAULT SCOPE DETECTION
// =====================================================
// defaultIsScopeSensitive decides whether this module likely needs dept/piyasa checks.
// You can tighten/extend later.
func defaultIsScopeSensitive(module string, r *http.Request) bool {
switch module {
case "order", "customer", "report", "finance":
return true
default:
return false
}
}
// =====================================================
// 🔎 DEFAULT EXTRACTORS
// =====================================================
// We try to extract scope from:
// - query params: dprt, dprt_code, department, department_code, piyasa, piyasa_code, market
// - headers: X-Department, X-Piyasa
// - json body fields: department / department_code / dprt_code, piyasa / piyasa_code / market_code
// (body read is safe: we re-inject the body)
func defaultExtractDepartmentCodes(r *http.Request) []string {
var out []string
// query params
for _, k := range []string{"dprt", "dprt_code", "department", "department_code"} {
out = append(out, splitCSV(r.URL.Query().Get(k))...)
}
// headers
out = append(out, splitCSV(r.Header.Get("X-Department"))...)
// JSON body (if any)
out = append(out, extractFromJSONBody(r, []string{
"department", "department_code", "dprt", "dprt_code",
})...)
return out
}
func defaultExtractPiyasaCodes(r *http.Request) []string {
var out []string
for _, k := range []string{"piyasa", "piyasa_code", "market", "market_code"} {
out = append(out, splitCSV(r.URL.Query().Get(k))...)
}
out = append(out, splitCSV(r.Header.Get("X-Piyasa"))...)
out = append(out, extractFromJSONBody(r, []string{
"piyasa", "piyasa_code", "market", "market_code", "customer_attribute",
})...)
return out
}
func extractFromJSONBody(r *http.Request, keys []string) []string {
// Only for methods that might have body
switch r.Method {
case http.MethodPost, http.MethodPut, http.MethodPatch:
default:
return nil
}
// read body (and restore)
raw, err := readBodyAndRestore(r, maxBodyRead)
if err != nil || len(raw) == 0 {
return nil
}
// try parse object
var obj map[string]any
if err := json.Unmarshal(raw, &obj); err != nil {
return nil
}
var out []string
for _, k := range keys {
if v, ok := obj[k]; ok {
switch t := v.(type) {
case string:
out = append(out, splitCSV(t)...)
case []any:
for _, it := range t {
if s, ok := it.(string); ok {
out = append(out, splitCSV(s)...)
}
}
}
}
}
return out
}
func readBodyAndRestore(r *http.Request, limit int64) ([]byte, error) {
if r.Body == nil {
return nil, nil
}
// Read with limit
raw, err := io.ReadAll(io.LimitReader(r.Body, limit))
if err != nil {
return nil, err
}
// restore
r.Body = io.NopCloser(bytes.NewBuffer(raw))
return raw, nil
}
// =====================================================
// 🧼 HELPERS
// =====================================================
func splitCSV(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func normalizeCodes(in []string) []string {
if len(in) == 0 {
return nil
}
seen := map[string]struct{}{}
out := make([]string, 0, len(in))
for _, s := range in {
s = strings.ToUpper(strings.TrimSpace(s))
if s == "" {
continue
}
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
return out
}
// pqArray: minimal adapter to pass []string as Postgres array.
// If you already use lib/pq, replace this with pq.Array.
// Here we use a simple JSON array -> Postgres can cast text[] from ARRAY[]? Not directly.
// So: YOU SHOULD USE lib/pq in your project. If it's already there, change pqArray() to pq.Array(slice).
//
// For now, we implement as a driver.Value using "{A,B}" format (Postgres text[] literal).
type pgTextArray string
func (a pgTextArray) Value() (any, error) { return string(a), nil }
func pqArray(ss []string) any {
// produce "{A,B,C}" as text[] literal
// escape quotes minimally
if len(ss) == 0 {
return pgTextArray("{}")
}
var b strings.Builder
b.WriteString("{")
for i, s := range ss {
if i > 0 {
b.WriteString(",")
}
s = strings.ReplaceAll(s, `"`, `\"`)
b.WriteString(`"`)
b.WriteString(s)
b.WriteString(`"`)
}
b.WriteString("}")
return pgTextArray(b.String())
}
func itoa(n int64) string {
return strconv.FormatInt(n, 10)
}
// isolate strconv usage without importing it globally in this snippet
func strconvFormatInt(n int64) string {
// local minimal
// NOTE: in real code just import strconv and use strconv.FormatInt(n, 10)
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var buf [32]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + (n % 10))
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}
// optional: allow passing scope explicitly from handlers via context (advanced use)
type scopeKey string
// =====================================================
// 🔍 ROLE RESOLVER (code -> id) WITH CACHE
// =====================================================
func cachedRoleID(pg *sql.DB, c *ttlCache, roleCode string) (int64, error) {
key := "role|" + strings.ToLower(roleCode)
if v, ok := c.get(key); ok {
return v.(int64), nil
}
var id int64
err := pg.QueryRow(`
SELECT id
FROM dfrole
WHERE LOWER(code) = LOWER($1)
`, roleCode).Scan(&id)
if err != nil {
return 0, err
}
c.set(key, id)
return id, nil
}
// =====================================================
// 🧹 CACHE INVALIDATION (ADMIN)
// =====================================================
func ClearAuthzScopeCacheForUser(userID int64) {
// NOTE: this clears ALL scope cache.
// Simple & safe. Optimize later if needed.
if globalScopeCache != nil {
globalScopeCache.mu.Lock()
defer globalScopeCache.mu.Unlock()
for k := range globalScopeCache.m {
if strings.Contains(k, "|"+itoa(userID)+"|") {
delete(globalScopeCache.m, k)
}
}
}
}
// intersect: A ∩ B
func intersect(a, b []string) []string {
set := make(map[string]struct{}, len(a))
for _, v := range a {
set[v] = struct{}{}
}
var out []string
for _, v := range b {
if _, ok := set[v]; ok {
out = append(out, v)
}
}
return out
}
func AuthzGuardByRoute(pg *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// =====================================================
// 0⃣ OPTIONS → PASS (CORS preflight)
// =====================================================
if r.Method == http.MethodOptions {
next.ServeHTTP(w, r)
return
}
// =====================================================
// 1⃣ AUTH
// =====================================================
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// =====================================================
// 2⃣ REAL ROUTE TEMPLATE ( /api/users/{id} )
// =====================================================
route := mux.CurrentRoute(r)
if route == nil {
log.Printf("❌ AUTHZ: route not resolved: %s %s",
r.Method, r.URL.Path,
)
http.Error(w, "route not resolved", 403)
return
}
pathTemplate, err := route.GetPathTemplate()
if err != nil {
log.Printf("❌ AUTHZ: path template error: %v", err)
http.Error(w, "route template error", 403)
return
}
// =====================================================
// 3⃣ ROUTE LOOKUP (path + method)
// =====================================================
var module, action string
err = pg.QueryRow(`
SELECT module_code, action
FROM mk_sys_routes
WHERE path = $1
AND method = $2
`,
pathTemplate,
r.Method,
).Scan(&module, &action)
if err != nil {
log.Printf(
"❌ AUTHZ: route not registered: %s %s",
r.Method,
pathTemplate,
)
http.Error(w, "route permission not found", 403)
return
}
// =====================================================
// 4⃣ PERMISSION RESOLVE
// =====================================================
repo := permissions.NewPermissionRepository(pg)
allowed, err := repo.ResolvePermissionChain(
int64(claims.ID),
int64(claims.RoleID),
claims.DepartmentCodes,
module,
action,
)
if err != nil {
log.Printf(
"❌ AUTHZ: resolve error user=%d %s:%s err=%v",
claims.ID,
module,
action,
err,
)
http.Error(w, "forbidden", 403)
return
}
if !allowed {
log.Printf(
"⛔ AUTHZ: denied user=%d %s:%s",
claims.ID,
module,
action,
)
http.Error(w, "forbidden", 403)
return
}
// =====================================================
// 5⃣ PASS
// =====================================================
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,24 @@
package middlewares
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/utils"
"net/http"
)
func CurrentUser(r *http.Request) (*models.User, bool) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
return nil, false
}
user := utils.UserFromClaims(claims)
if user == nil {
return nil, false
}
return user, true
}

View File

@@ -0,0 +1,66 @@
package middlewares
import (
"bssapp-backend/auth"
"database/sql"
"log"
"net/http"
"strings"
)
// 🔓 force_password_change=true iken izinli endpoint prefixleri
var passwordChangeAllowlist = []string{
"/api/password/change",
"/api/password/reset",
"/api/password/reset/validate",
"/api/auth/refresh",
}
func ForcePasswordChangeGuard(db *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
log.Println("❌ FPC GUARD: claims NOT FOUND")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
log.Printf(
"🛡️ FPC GUARD user=%s force=%v path=%s",
claims.Username,
claims.ForcePasswordChange,
r.URL.Path,
)
// 🔓 Şifre değişimi zorunlu DEĞİL → serbest
if !claims.ForcePasswordChange {
next.ServeHTTP(w, r)
return
}
// 🔐 Şifre değişimi ZORUNLU → allowlist kontrolü
for _, allowed := range passwordChangeAllowlist {
if strings.HasPrefix(r.URL.Path, allowed) {
log.Printf(
"✅ FPC GUARD PASS user=%s path=%s",
claims.Username,
r.URL.Path,
)
next.ServeHTTP(w, r)
return
}
}
// ⛔ Zorunlu ama yanlış endpoint
log.Printf(
"⛔ FPC GUARD BLOCK user=%s path=%s",
claims.Username,
r.URL.Path,
)
http.Error(w, "password change required", http.StatusUnauthorized)
})
}
}

View File

@@ -0,0 +1,58 @@
package middlewares
import (
"bssapp-backend/auth"
"log"
"net/http"
"strings"
)
var publicPaths = []string{
"/api/auth/login",
"/api/auth/refresh",
"/api/password/forgot",
"/api/password/reset",
}
func GlobalAuthMiddleware(db any, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// PUBLIC ROUTES
for _, p := range publicPaths {
if strings.HasPrefix(path, p) {
next.ServeHTTP(w, r)
return
}
}
// JWT
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
claims, err := auth.ValidateToken(parts[1])
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := auth.WithClaims(r.Context(), claims)
log.Printf("🔐 GLOBAL AUTH user=%d role=%s",
claims.ID,
claims.RoleCode,
)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -0,0 +1,56 @@
package middlewares
import (
"bssapp-backend/auth"
"bssapp-backend/ctxkeys"
"context"
)
func getClaims(ctx context.Context) *auth.Claims {
if v := ctx.Value(ctxkeys.UserContextKey); v != nil {
if c, ok := v.(*auth.Claims); ok {
return c
}
}
return nil
}
// --------------------------------------------------
// 🔐 SESSION
// --------------------------------------------------
func GetSessionID(ctx context.Context) string {
if c := getClaims(ctx); c != nil {
return c.SessionID
}
return ""
}
// --------------------------------------------------
// 🔑 USER ID (mk_dfusr.id)
// --------------------------------------------------
func GetUserID(ctx context.Context) int64 {
if c := getClaims(ctx); c != nil {
return c.ID
}
return 0
}
// --------------------------------------------------
// 👤 USERNAME
// --------------------------------------------------
func GetUsername(ctx context.Context) string {
if c := getClaims(ctx); c != nil {
return c.Username
}
return ""
}
// --------------------------------------------------
// 🧩 ROLE
// --------------------------------------------------
func GetRoleCode(ctx context.Context) string {
if c := getClaims(ctx); c != nil && c.RoleCode != "" {
return c.RoleCode
}
return "public"
}

View File

@@ -0,0 +1,59 @@
package middlewares
import (
"net"
"net/http"
"sync"
"time"
)
type rateEntry struct {
Count int
ExpiresAt time.Time
}
var (
rateMu sync.Mutex
rateDB = make(map[string]*rateEntry)
)
func RateLimit(keyFn func(*http.Request) string, limit int, window time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := keyFn(r)
now := time.Now()
rateMu.Lock()
e, ok := rateDB[key]
if !ok || now.After(e.ExpiresAt) {
e = &rateEntry{
Count: 0,
ExpiresAt: now.Add(window),
}
rateDB[key] = e
}
e.Count++
if e.Count > limit {
rateMu.Unlock()
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
rateMu.Unlock()
next.ServeHTTP(w, r)
})
}
}
// helpers
func RateByIP(r *http.Request) string {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return "ip:" + ip
}
func RateByUser(r *http.Request) string {
return "user:" + r.URL.Path // id pathten okunabilir
}

View File

@@ -0,0 +1,108 @@
package middlewares
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"log"
"net"
"net/http"
"time"
)
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}
func RequestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{
ResponseWriter: w,
status: 200,
}
// ---------- CLAIMS ----------
claims, _ := auth.GetClaimsFromContext(r.Context())
// ---------- IP ----------
ip := r.RemoteAddr
if host, _, err := net.SplitHostPort(ip); err == nil {
ip = host
}
// ---------- UA ----------
ua := r.UserAgent()
// ---------- SESSION ----------
sessionID := ""
if claims != nil {
sessionID = claims.SessionID
}
hasAuth := r.Header.Get("Authorization") != ""
log.Printf("➡️ %s %s | auth=%v", r.Method, r.URL.Path, hasAuth)
// ---------- RUN ----------
next.ServeHTTP(sw, r)
finish := time.Now()
dur := int(finish.Sub(start).Milliseconds())
log.Printf("⬅️ %s %s | status=%d | %s", r.Method, r.URL.Path, sw.status, time.Since(start))
// ---------- AUDIT (route_access) ----------
al := auditlog.ActivityLog{
ActionType: "route_access",
ActionCategory: "nav",
ActionTarget: r.URL.Path,
Description: r.Method + " " + r.URL.Path,
IpAddress: ip,
UserAgent: ua,
SessionID: sessionID,
RequestStartedAt: start,
RequestFinishedAt: finish,
DurationMs: dur,
HttpStatus: sw.status,
IsSuccess: sw.status < 400,
}
// ---------- CLAIMS → LOG ----------
if claims != nil {
al.Username = claims.Username
al.RoleCode = claims.RoleCode
al.DfUsrID = int64(claims.ID)
// Eğer claims içinde UUID varsa ekle (sende varsa aç)
// al.UserID = claims.UserUUID
} else {
al.RoleCode = "public"
}
// ---------- ERROR ----------
if sw.status >= 400 {
al.ErrorMessage = http.StatusText(sw.status)
}
// ✅ ESKİ: auditlog.Write(al)
// ✅ YENİ:
auditlog.Enqueue(r.Context(), al)
if claims == nil {
log.Println("⚠️ LOGGER: claims is NIL")
} else {
log.Printf("✅ LOGGER CLAIMS user=%s role=%s id=%d", claims.Username, claims.RoleCode, claims.ID)
}
})
}

View File

@@ -0,0 +1,6 @@
package models
type NewOrderNumberResponse struct {
OrderHeaderID string `json:"OrderHeaderID"`
OrderNumber string `json:"OrderNumber"`
}

8
svc/models/account.go Normal file
View File

@@ -0,0 +1,8 @@
package models
// Cari hesap modeli
type Account struct {
DisplayCode string `json:"display_code"` // "ZLA 0127"
AccountCode string `json:"account_code"` // "ZLA0127"
AccountName string `json:"account_name"` // "EKO TEXTIL COM SRL" ya da ""
}

45
svc/models/custom_time.go Normal file
View File

@@ -0,0 +1,45 @@
package models
import (
"database/sql"
"strings"
"time"
)
type CustomTime struct {
sql.NullTime
}
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), `"`)
if s == "" || s == "null" {
ct.Valid = false
return nil
}
// DESTEKLENEN TÜM FORMATLAR
layouts := []string{
time.RFC3339, // 2025-11-21T00:10:27Z
"2006-01-02T15:04:05", // 2025-11-21T00:10:27
"2006-01-02 15:04:05", // 2025-11-21 00:10:27 ← FRONTEND FORMATIN!
}
for _, layout := range layouts {
if t, err := time.Parse(layout, s); err == nil {
ct.Time = t
ct.Valid = true
return nil
}
}
// Hâlâ parse edemediyse → invalid kabul et
ct.Valid = false
return nil
}
func (ct CustomTime) MarshalJSON() ([]byte, error) {
if ct.Valid {
return []byte(`"` + ct.Time.Format("2006-01-02 15:04:05") + `"`), nil
}
return []byte(`null`), nil
}

View File

@@ -0,0 +1,13 @@
// models/customerlist.go
package models
type CustomerList struct {
CurrAccTypeCode int `json:"CurrAccTypeCode"` // 🆕 FK için lazım
Cari_Kod string `json:"Cari_Kod"`
Cari_Ad string `json:"Cari_Ad"`
Musteri_Ana_Grubu string `json:"Musteri_Ana_Grubu"`
Piyasa string `json:"Piyasa"`
Musteri_Temsilcisi string `json:"Musteri_Temsilcisi"`
Ulke string `json:"Ulke"`
Doviz_cinsi string `json:"Doviz_Cinsi"`
}

View File

@@ -0,0 +1,13 @@
package models
type InvalidVariant struct {
Index int `json:"index"`
ClientKey string `json:"clientKey"`
ItemCode string `json:"itemCode"`
ColorCode string `json:"colorCode"`
Dim1 string `json:"dim1"`
Dim2 string `json:"dim2"`
Qty1 float64 `json:"qty1"`
ComboKey string `json:"comboKey,omitempty"`
Reason string `json:"reason,omitempty"`
}

57
svc/models/mk_user.go Normal file
View File

@@ -0,0 +1,57 @@
package models
import "time"
type MkUser struct {
// ==================================================
// 🔑 PRIMARY ID (mk_dfusr.id)
// ==================================================
ID int64 `json:"id"` // mk_dfusr.id
RoleID int64 `json:"role_id"`
// ==================================================
// 🔁 LEGACY DFUSR (opsiyonel migrate/debug)
// ==================================================
LegacyDfUsrID *int64 `json:"legacy_dfusr_id,omitempty"`
// ==================================================
// 👤 CORE
// ==================================================
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
Mobile string `json:"mobile"`
IsActive bool `json:"is_active"`
// ==================================================
// 🔐 AUTH
// ==================================================
PasswordHash string `json:"-"`
ForcePasswordChange bool `json:"force_password_change"`
PasswordUpdatedAt *time.Time `json:"password_updated_at,omitempty"`
SessionID string `json:"session_id"`
// ==================================================
// 🧩 ROLE
// ==================================================
RoleCode string `json:"role_code"`
// ==================================================
// 🧾 NEBIM
// ==================================================
V3Username string `json:"v3_username"`
V3UserGroup string `json:"v3_usergroup"`
// ==================================================
// 🏭 RUNTIME SCOPES
// ==================================================
DepartmentCodes []string `json:"departments"`
PiyasaCodes []string `json:"piyasalar"`
// ==================================================
// ⏱ AUDIT
// ==================================================
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
}

View File

@@ -0,0 +1,16 @@
// models/errors.go
package models
type ValidationError struct {
Code string `json:"code"`
Message string `json:"message"`
ClientKey string `json:"clientKey,omitempty"`
ItemCode string `json:"itemCode,omitempty"`
ColorCode string `json:"colorCode,omitempty"`
Dim1 string `json:"dim1,omitempty"`
Dim2 string `json:"dim2,omitempty"`
}
func (e *ValidationError) Error() string {
return e.Message
}

120
svc/models/null_uuid.go Normal file
View File

@@ -0,0 +1,120 @@
package models
import (
"database/sql/driver"
"encoding/json"
"strings"
)
// ============================================================
// 🔹 NullUUID — MSSQL UNIQUEIDENTIFIER için nullable tip
// ============================================================
type NullUUID struct {
UUID string `json:"UUID"`
Valid bool `json:"Valid"`
}
// ------------------------------------------------------------
//
// SQL Valuer (INSERT/UPDATE parametresi)
//
// ------------------------------------------------------------
func (n NullUUID) Value() (driver.Value, error) {
if !n.Valid || strings.TrimSpace(n.UUID) == "" {
return nil, nil
}
return n.UUID, nil
}
// ------------------------------------------------------------
//
// SQL Scanner (SELECT sonucu)
//
// ------------------------------------------------------------
func (n *NullUUID) Scan(value interface{}) error {
if value == nil {
n.UUID = ""
n.Valid = false
return nil
}
switch v := value.(type) {
case string:
n.UUID = v
n.Valid = true
case []byte:
n.UUID = string(v)
n.Valid = true
default:
n.UUID = ""
n.Valid = false
}
return nil
}
// ------------------------------------------------------------
//
// JSON Marshal — Go → JSON
// Null ise: null
// Değer varsa: "guid-string"
//
// ------------------------------------------------------------
func (n NullUUID) MarshalJSON() ([]byte, error) {
if !n.Valid || strings.TrimSpace(n.UUID) == "" {
return []byte("null"), nil
}
return json.Marshal(n.UUID)
}
// ------------------------------------------------------------
//
// JSON Unmarshal — JSON → Go
// Kabul ettiği formatlar:
// • null
// • "guid-string"
// • {"UUID":"...","Valid":true} (ileride istersen)
//
// ------------------------------------------------------------
func (n *NullUUID) UnmarshalJSON(data []byte) error {
s := strings.TrimSpace(string(data))
// null veya boş
if s == "null" || s == "" {
n.UUID = ""
n.Valid = false
return nil
}
// Önce string gibi dene: "guid"
var str string
if err := json.Unmarshal(data, &str); err == nil {
str = strings.TrimSpace(str)
if str == "" {
n.UUID = ""
n.Valid = false
} else {
n.UUID = str
n.Valid = true
}
return nil
}
// Objeyse: { "UUID":"...", "Valid":true }
var aux struct {
UUID string `json:"UUID"`
Valid bool `json:"Valid"`
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
aux.UUID = strings.TrimSpace(aux.UUID)
if aux.UUID == "" || !aux.Valid {
n.UUID = ""
n.Valid = false
} else {
n.UUID = aux.UUID
n.Valid = true
}
return nil
}

54
svc/models/order_pdf.go Normal file
View File

@@ -0,0 +1,54 @@
package models
// ===============================
// 🔹 ORDER HEADER (PDF için)
// ===============================
type OrderHeaderPDF struct {
OrderHeaderID string `json:"OrderHeaderID"`
OrderNumber string `json:"OrderNumber"`
CurrAccCode string `json:"CurrAccCode"`
CariAdi string `json:"CariAdi"`
OrderDate string `json:"OrderDate"` // yyyy-MM-dd
AverageDueDate string `json:"AverageDueDate"` // Termin
Description string `json:"Description"`
}
// ===============================
// 🔹 ORDER LINE (PDF için)
// ===============================
type OrderLinePDF struct {
OrderLineID string `json:"OrderLineID"`
OrderHeaderID string `json:"OrderHeaderID"`
Model string `json:"Model"`
Renk string `json:"Renk"`
Renk2 string `json:"Renk2"`
AnaGrup string `json:"AnaGrup"`
AltGrup string `json:"AltGrup"`
Aciklama string `json:"Aciklama"`
Fiyat float64 `json:"Fiyat"`
Adet float64 `json:"Adet"`
Tutar float64 `json:"Tutar"`
PB string `json:"PB"`
TerminTarihi string `json:"TerminTarihi"`
// 16 beden alanı
Size1 float64 `json:"Size1"`
Size2 float64 `json:"Size2"`
Size3 float64 `json:"Size3"`
Size4 float64 `json:"Size4"`
Size5 float64 `json:"Size5"`
Size6 float64 `json:"Size6"`
Size7 float64 `json:"Size7"`
Size8 float64 `json:"Size8"`
Size9 float64 `json:"Size9"`
Size10 float64 `json:"Size10"`
Size11 float64 `json:"Size11"`
Size12 float64 `json:"Size12"`
Size13 float64 `json:"Size13"`
Size14 float64 `json:"Size14"`
Size15 float64 `json:"Size15"`
Size16 float64 `json:"Size16"`
}

98
svc/models/orderdetail.go Normal file
View File

@@ -0,0 +1,98 @@
package models
// ============================================================
// 📦 ORDER DETAIL (trOrderLine)
// ============================================================
type OrderDetail struct {
OrderLineID string `json:"OrderLineID"`
// 🔑 Frontendden gelen benzersiz anahtar (sadece JSON, DB kolonu değil)
ClientKey NullString `json:"clientKey"`
ComboKey NullString `json:"ComboKey"`
SortOrder NullInt32 `json:"SortOrder"`
ItemTypeCode NullInt16 `json:"ItemTypeCode"`
ItemCode NullString `json:"ItemCode"`
ColorCode NullString `json:"ColorCode"`
ItemDim1Code NullString `json:"ItemDim1Code"`
ItemDim2Code NullString `json:"ItemDim2Code"`
ItemDim3Code NullString `json:"ItemDim3Code"`
Qty1 NullFloat64 `json:"Qty1"`
Qty2 NullFloat64 `json:"Qty2"`
CancelQty1 NullFloat64 `json:"CancelQty1"`
CancelQty2 NullFloat64 `json:"CancelQty2"`
CancelDate NullTime `json:"CancelDate"`
OrderCancelReasonCode NullString `json:"OrderCancelReasonCode"`
ClosedDate NullTime `json:"ClosedDate"`
IsClosed NullBool `json:"IsClosed"`
SalespersonCode NullString `json:"SalespersonCode"`
PaymentPlanCode NullString `json:"PaymentPlanCode"`
PurchasePlanCode NullString `json:"PurchasePlanCode"`
// MSSQL smalldatetime
DeliveryDate NullTime `json:"DeliveryDate"`
// MSSQL date (YYYY-MM-DD)
PlannedDateOfLading NullString `json:"PlannedDateOfLading"`
LineDescription NullString `json:"LineDescription"`
UsedBarcode NullString `json:"UsedBarcode"`
CostCenterCode NullString `json:"CostCenterCode"`
VatCode NullString `json:"VatCode"`
VatRate NullFloat64 `json:"VatRate"`
PCTCode NullString `json:"PCTCode"`
PCTRate NullFloat64 `json:"PCTRate"`
LDisRate1 NullFloat64 `json:"LDisRate1"`
LDisRate2 NullFloat64 `json:"LDisRate2"`
LDisRate3 NullFloat64 `json:"LDisRate3"`
LDisRate4 NullFloat64 `json:"LDisRate4"`
LDisRate5 NullFloat64 `json:"LDisRate5"`
DocCurrencyCode NullString `json:"DocCurrencyCode"`
PriceCurrencyCode NullString `json:"PriceCurrencyCode"`
PriceExchangeRate NullFloat64 `json:"PriceExchangeRate"`
Price NullFloat64 `json:"Price"`
PriceListLineID NullString `json:"PriceListLineID"`
BaseProcessCode NullString `json:"BaseProcessCode"`
BaseOrderNumber NullString `json:"BaseOrderNumber"`
BaseCustomerTypeCode NullInt32 `json:"BaseCustomerTypeCode"`
BaseCustomerCode NullString `json:"BaseCustomerCode"`
BaseSubCurrAccID NullString `json:"BaseSubCurrAccID"`
BaseStoreCode NullString `json:"BaseStoreCode"`
SupportRequestHeaderID NullString `json:"SupportRequestHeaderID"`
OrderHeaderID string `json:"OrderHeaderID"`
OrderLineSumID NullInt32 `json:"OrderLineSumID"`
OrderLineBOMID NullInt32 `json:"OrderLineBOMID"`
CreatedUserName NullString `json:"CreatedUserName"`
CreatedDate NullString `json:"CreatedDate"`
LastUpdatedUserName NullString `json:"LastUpdatedUserName"`
LastUpdatedDate NullString `json:"LastUpdatedDate"`
SurplusOrderQtyToleranceRate NullFloat64 `json:"SurplusOrderQtyToleranceRate"`
PurchaseRequisitionLineID NullString `json:"PurchaseRequisitionLineID"`
WithHoldingTaxTypeCode NullString `json:"WithHoldingTaxTypeCode"`
DOVCode NullString `json:"DOVCode"`
OrderLineLinkedProductID NullString `json:"OrderLineLinkedProductID"`
// JOIN attr
UrunIlkGrubu NullString `json:"UrunIlkGrubu"`
UrunAnaGrubu NullString `json:"UrunAnaGrubu"`
UrunAltGrubu NullString `json:"UrunAltGrubu"`
Fit1 NullString `json:"Fit1"`
Fit2 NullString `json:"Fit2"`
}

109
svc/models/orderheader.go Normal file
View File

@@ -0,0 +1,109 @@
package models
// ============================================================
// 🧾 ORDER HEADER
// ============================================================
type OrderHeader struct {
OrderHeaderID string `json:"OrderHeaderID"`
OrderTypeCode NullInt16 `json:"OrderTypeCode"`
ProcessCode NullString `json:"ProcessCode"`
OrderNumber NullString `json:"OrderNumber"`
IsCancelOrder NullBool `json:"IsCancelOrder"`
// 🔥 Date & Time string olarak geliyor (frontend)
OrderDate NullString `json:"OrderDate"` // "YYYY-MM-DD"
OrderTime NullString `json:"OrderTime"` // "HH:mm:ss"
DocumentNumber NullString `json:"DocumentNumber"`
PaymentTerm NullInt16 `json:"PaymentTerm"`
AverageDueDate NullString `json:"AverageDueDate"` // "YYYY-MM-DD"
Description NullString `json:"Description"`
InternalDescription NullString `json:"InternalDescription"`
CurrAccTypeCode NullInt16 `json:"CurrAccTypeCode"`
CurrAccCode NullString `json:"CurrAccCode"`
CurrAccDescription NullString `json:"CurrAccDescription"`
// 🔥 GUID alanları NullUUID yapıldı
SubCurrAccID NullUUID `json:"SubCurrAccID"`
ContactID NullUUID `json:"ContactID"`
ShipmentMethodCode NullString `json:"ShipmentMethodCode"`
ShippingPostalAddressID NullUUID `json:"ShippingPostalAddressID"`
BillingPostalAddressID NullUUID `json:"BillingPostalAddressID"`
GuarantorContactID NullUUID `json:"GuarantorContactID"`
GuarantorContactID2 NullUUID `json:"GuarantorContactID2"`
RoundsmanCode NullString `json:"RoundsmanCode"`
DeliveryCompanyCode NullString `json:"DeliveryCompanyCode"`
TaxTypeCode NullInt16 `json:"TaxTypeCode"`
WithHoldingTaxTypeCode NullString `json:"WithHoldingTaxTypeCode"`
DOVCode NullString `json:"DOVCode"`
TaxExemptionCode NullInt16 `json:"TaxExemptionCode"`
CompanyCode NullInt32 `json:"CompanyCode"`
OfficeCode NullString `json:"OfficeCode"`
StoreTypeCode NullInt16 `json:"StoreTypeCode"`
StoreCode NullString `json:"StoreCode"`
POSTerminalID NullInt16 `json:"POSTerminalID"`
WarehouseCode NullString `json:"WarehouseCode"`
ToWarehouseCode NullString `json:"ToWarehouseCode"`
OrdererCompanyCode NullInt32 `json:"OrdererCompanyCode"`
OrdererOfficeCode NullString `json:"OrdererOfficeCode"`
OrdererStoreCode NullString `json:"OrdererStoreCode"`
GLTypeCode NullString `json:"GLTypeCode"`
DocCurrencyCode NullString `json:"DocCurrencyCode"`
LocalCurrencyCode NullString `json:"LocalCurrencyCode"`
ExchangeRate NullFloat64 `json:"ExchangeRate"`
TDisRate1 NullFloat64 `json:"TDisRate1"`
TDisRate2 NullFloat64 `json:"TDisRate2"`
TDisRate3 NullFloat64 `json:"TDisRate3"`
TDisRate4 NullFloat64 `json:"TDisRate4"`
TDisRate5 NullFloat64 `json:"TDisRate5"`
DiscountReasonCode NullInt16 `json:"DiscountReasonCode"`
SurplusOrderQtyToleranceRate NullFloat64 `json:"SurplusOrderQtyToleranceRate"`
ImportFileNumber NullString `json:"ImportFileNumber"`
ExportFileNumber NullString `json:"ExportFileNumber"`
IncotermCode1 NullString `json:"IncotermCode1"`
IncotermCode2 NullString `json:"IncotermCode2"`
LettersOfCreditNumber NullString `json:"LettersOfCreditNumber"`
PaymentMethodCode NullString `json:"PaymentMethodCode"`
IsInclutedVat NullBool `json:"IsInclutedVat"`
IsCreditSale NullBool `json:"IsCreditSale"`
IsCreditableConfirmed NullBool `json:"IsCreditableConfirmed"`
CreditableConfirmedUser NullString `json:"CreditableConfirmedUser"`
// MSSQL datetime
CreditableConfirmedDate CustomTime `json:"CreditableConfirmedDate"`
IsSalesViaInternet NullBool `json:"IsSalesViaInternet"`
IsSuspended NullBool `json:"IsSuspended"`
IsCompleted NullBool `json:"IsCompleted"`
IsPrinted NullBool `json:"IsPrinted"`
IsLocked NullBool `json:"IsLocked"`
UserLocked NullBool `json:"UserLocked"`
IsClosed NullBool `json:"IsClosed"`
ApplicationCode NullString `json:"ApplicationCode"`
ApplicationID NullUUID `json:"ApplicationID"`
CreatedUserName NullString `json:"CreatedUserName"`
// Frontend "YYYY-MM-DD HH:mm:ss" gönderiyor
CreatedDate NullString `json:"CreatedDate"`
LastUpdatedUserName NullString `json:"LastUpdatedUserName"`
LastUpdatedDate NullString `json:"LastUpdatedDate"`
IsProposalBased NullBool `json:"IsProposalBased"`
}

View File

@@ -0,0 +1,10 @@
package models
type OrderInventory struct {
UrunKodu string `json:"urun_kodu"`
RenkKodu string `json:"renk_kodu"`
RenkAciklamasi string `json:"renk_aciklamasi"`
Beden string `json:"beden"`
Yaka string `json:"yaka"`
KullanilabilirEnvanter float64 `json:"kullanilabilir_envanter"`
}

35
svc/models/orderlist.go Normal file
View File

@@ -0,0 +1,35 @@
package models
// ========================================================
// 📌 OrderList — Sipariş Listeleme Modeli (FINAL & UI SAFE)
// ========================================================
type OrderList struct {
// 🆔 Sipariş Bilgileri
OrderHeaderID string `json:"OrderHeaderID"`
OrderNumber string `json:"OrderNumber"`
OrderDate string `json:"OrderDate"`
// 🧾 Cari Bilgileri
CurrAccCode string `json:"CurrAccCode"`
CurrAccDescription string `json:"CurrAccDescription"`
// 👤 Müşteri Tanımları
MusteriTemsilcisi string `json:"MusteriTemsilcisi"`
Piyasa string `json:"Piyasa"`
// Sipariş Durumu
CreditableConfirmedDate string `json:"CreditableConfirmedDate"`
IsCreditableConfirmed bool `json:"IsCreditableConfirmed"`
// 💱 Para Birimi
DocCurrencyCode string `json:"DocCurrencyCode"`
// 💰 Tutarlar
TotalAmount float64 `json:"TotalAmount"`
TotalAmountUSD float64 `json:"TotalAmountUSD"`
// 📝 Açıklama
Description string `json:"Description"`
ExchangeRateUSD float64 `json:"ExchangeRateUSD"`
}

View File

@@ -0,0 +1,179 @@
package models
import (
"database/sql"
"encoding/json"
"time"
)
/* ==============================================================
🔹 NULL TYPE WRAPPERS
MSSQL sql.Null* tiplerini JSON'a dönüştürürken:
- null yerine "", 0 veya false döndürür
- frontend'de (Vue/Quasar) doğrudan okunabilir hale getirir
============================================================== */
// 🟦 STRING
type NullString struct {
sql.NullString
}
func (ns *NullString) MarshalJSON() ([]byte, error) {
if ns.Valid {
return json.Marshal(ns.String)
}
return json.Marshal("")
}
func (ns *NullString) UnmarshalJSON(b []byte) error {
var s *string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
if s != nil {
ns.Valid = true
ns.String = *s
} else {
ns.Valid = false
ns.String = ""
}
return nil
}
// 🟩 INT32
type NullInt32 struct {
sql.NullInt32
}
func (ni *NullInt32) MarshalJSON() ([]byte, error) {
if ni.Valid {
return json.Marshal(ni.Int32)
}
return json.Marshal(0)
}
func (ni *NullInt32) UnmarshalJSON(b []byte) error {
var n *int32
if err := json.Unmarshal(b, &n); err != nil {
return err
}
if n != nil {
ni.Valid = true
ni.Int32 = *n
} else {
ni.Valid = false
ni.Int32 = 0
}
return nil
}
// 🟧 INT16
type NullInt16 struct {
sql.NullInt16
}
func (ni *NullInt16) MarshalJSON() ([]byte, error) {
if ni.Valid {
return json.Marshal(ni.Int16)
}
return json.Marshal(0)
}
func (ni *NullInt16) UnmarshalJSON(b []byte) error {
var n *int16
if err := json.Unmarshal(b, &n); err != nil {
return err
}
if n != nil {
ni.Valid = true
ni.Int16 = *n
} else {
ni.Valid = false
ni.Int16 = 0
}
return nil
}
// 🟨 FLOAT64
type NullFloat64 struct {
sql.NullFloat64
}
func (nf *NullFloat64) MarshalJSON() ([]byte, error) {
if nf.Valid {
return json.Marshal(nf.Float64)
}
return json.Marshal(0.0)
}
func (nf *NullFloat64) UnmarshalJSON(b []byte) error {
var f *float64
if err := json.Unmarshal(b, &f); err != nil {
return err
}
if f != nil {
nf.Valid = true
nf.Float64 = *f
} else {
nf.Valid = false
nf.Float64 = 0
}
return nil
}
// 🟥 BOOL
type NullBool struct {
sql.NullBool
}
func (nb *NullBool) MarshalJSON() ([]byte, error) {
if nb.Valid {
return json.Marshal(nb.Bool)
}
return json.Marshal(false)
}
func (nb *NullBool) UnmarshalJSON(b []byte) error {
var v *bool
if err := json.Unmarshal(b, &v); err != nil {
return err
}
if v != nil {
nb.Valid = true
nb.Bool = *v
} else {
nb.Valid = false
nb.Bool = false
}
return nil
}
// 🟪 TIME
type NullTime struct {
sql.NullTime
}
func (nt *NullTime) MarshalJSON() ([]byte, error) {
if nt.Valid {
return json.Marshal(nt.Time.Format("2006-01-02 15:04:05"))
}
return json.Marshal(nil)
}
func (nt *NullTime) UnmarshalJSON(b []byte) error {
var s *string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
if s != nil && *s != "" {
t, err := time.Parse("2006-01-02 15:04:05", *s)
if err != nil {
return err
}
nt.Valid = true
nt.Time = t
} else {
nt.Valid = false
}
return nil
}

View File

@@ -0,0 +1,13 @@
package models
// OrderPriceListB2B B2B ürün fiyat listesini temsil eder
type OrderPriceListB2B struct {
ModelCode string `json:"modelCode"`
CurrencyCode string `json:"currencyCode"`
Price float64 `json:"price"`
PriceGroupID int `json:"priceGroupId"`
LastUpdate string `json:"lastUpdate"`
RateToTRY float64 `json:"rateToTRY"`
PriceTRY float64 `json:"priceTRY"`
BaseCurrency string `json:"baseCurrency"`
}

5
svc/models/product.go Normal file
View File

@@ -0,0 +1,5 @@
package models
type Product struct {
ProductCode string `json:"ProductCode"` // ✅ büyük P harf ile
}

View File

@@ -0,0 +1,7 @@
package models
type ProductColor struct {
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
}

View File

@@ -0,0 +1,9 @@
package models
// 🔹 Ürün renk + beden varyasyon listesi (açıklamalar çıkarıldı)
type ProductColorSize struct {
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ItemDim1Code string `json:"item_dim1_code"`
ItemDim2Code string `json:"item_dim2_code"`
}

View File

@@ -0,0 +1,14 @@
package models
type ProductDetail struct {
ProductCode string `json:"ProductCode"`
UrunIlkGrubu string `json:"UrunIlkGrubu"`
UrunAnaGrubu string `json:"UrunAnaGrubu"`
UrunAltGrubu string `json:"UrunAltGrubu"`
UrunIcerik string `json:"UrunIcerik"`
Drop string `json:"Drop"`
Kategori string `json:"Kategori"`
AskiliYan string `json:"AskiliYan"`
Fit1 string `json:"Fit1"`
Fit2 string `json:"Fit2"`
}

View File

@@ -0,0 +1,7 @@
package models
type ProductSecondColor struct {
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ItemDim2Code string `json:"item_dim2_code"`
}

View File

@@ -0,0 +1,17 @@
package models
// Detay tablo (alt satır)
type StatementDetail struct {
BelgeTarihi string `json:"belge_tarihi"` // Belge tarihi
BelgeRefNumarasi string `json:"belge_ref_numarasi"` // Belge referans numarası
UrunAnaGrubu string `json:"urun_ana_grubu"` // Ürün ana grubu
UrunAltGrubu string `json:"urun_alt_grubu"` // Ürün alt grubu
YetiskinGarson string `json:"yetiskin_garson"` // Yetişkin/Çocuk (garson)
Fit string `json:"fit"` // Fit bilgisi
Icerik string `json:"icerik"` // İçerik
UrunKodu string `json:"urun_kodu"` // Ürün kodu
UrunRengi string `json:"urun_rengi"` // Ürün rengi
ToplamAdet float64 `json:"toplam_adet"` // Toplam adet
ToplamFiyat float64 `json:"toplam_fiyat"` // Birim fiyat
ToplamTutar float64 `json:"toplam_tutar"` // Toplam tutar
}

View File

@@ -0,0 +1,41 @@
package models
import (
"database/sql"
"encoding/json"
)
type StatementHeader struct {
CariKod string `json:"cari_kod"`
CariIsim string `json:"cari_isim"`
BelgeTarihi string `json:"belge_tarihi"`
VadeTarihi string `json:"vade_tarihi"`
BelgeNo string `json:"belge_no"`
IslemTipi string `json:"islem_tipi"`
Aciklama string `json:"aciklama"`
ParaBirimi string `json:"para_birimi"`
Borc float64 `json:"borc"`
Alacak float64 `json:"alacak"`
Bakiye float64 `json:"bakiye"`
Parislemler sql.NullString `json:"parislemler"`
// 🔹 PDF için detaylar
Details []StatementDetail `json:"details,omitempty"`
}
// JSON dönüşümünde NULL değerleri "" yap
func (s StatementHeader) MarshalJSON() ([]byte, error) {
type Alias StatementHeader
return json.Marshal(&struct {
Parislemler string `json:"parislemler"`
*Alias
}{
Parislemler: func() string {
if s.Parislemler.Valid {
return s.Parislemler.String
}
return ""
}(),
Alias: (*Alias)(&s),
})
}

View File

@@ -0,0 +1,10 @@
package models
type StatementParams struct {
CariKod string `json:"cari_kod"`
StartDate string `json:"startdate"`
EndDate string `json:"enddate"`
AccountCode string `json:"accountcode"`
LangCode string `json:"langcode"`
Parislemler []string `json:"parislemler"` // ✅ slice olmalı
}

View File

@@ -0,0 +1,10 @@
package models
// TodayCurrencyV3 sistemdeki döviz kurlarını temsil eder
type TodayCurrencyV3 struct {
CurrencyCode string `json:"currencyCode"`
RelationCurrencyCode string `json:"relationCurrencyCode"`
ExchangeTypeCode int `json:"exchangeTypeCode"`
Rate float64 `json:"rate"`
Date string `json:"date"`
}

88
svc/models/user_detail.go Normal file
View File

@@ -0,0 +1,88 @@
package models
import "encoding/json"
// ======================================================
// 👤 USER DETAIL — GET RESPONSE
// UserDetail.vue formuna birebir
// ======================================================
type UserDetail struct {
ID int64 `json:"id"`
Code string `json:"code"`
IsActive bool `json:"is_active"`
FullName string `json:"full_name"`
Email string `json:"email"`
Mobile string `json:"mobile"`
Address string `json:"address"`
HasPassword bool `json:"has_password"` // 🔐 SADECE DURUM
// ===== İLİŞKİLER =====
Roles []string `json:"roles"`
Departments []DeptOption `json:"departments"`
Piyasalar []DeptOption `json:"piyasalar"`
NebimUsers []NebimOption `json:"nebim_users"`
}
// ======================================================
// ✍️ USER WRITE — PUT PAYLOAD
// ======================================================
type UserWrite struct {
Code string `json:"code"`
IsActive bool `json:"is_active"`
FullName string `json:"full_name"`
Email string `json:"email"`
Mobile string `json:"mobile"`
Address string `json:"address"`
Roles []string `json:"roles"`
Departments []DeptOption `json:"departments"`
Piyasalar []DeptOption `json:"piyasalar"`
NebimUsers []NebimOption `json:"nebim_users"`
}
// ======================================================
// 🔹 COMMON OPTION MODELS
// ======================================================
type DeptOption struct {
Code string `json:"code"`
Title string `json:"title,omitempty"`
}
// Flexible JSON decode
func (d *DeptOption) UnmarshalJSON(data []byte) error {
var raw struct {
Code any `json:"code"`
Title string `json:"title"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
d.Title = raw.Title
switch v := raw.Code.(type) {
case string:
d.Code = v
case []any:
if len(v) > 0 {
if s, ok := v[0].(string); ok {
d.Code = s
}
}
default:
d.Code = ""
}
return nil
}
type NebimOption struct {
Username string `json:"username"`
UserGroupCode string `json:"user_group_code"`
}

18
svc/models/user_list.go Normal file
View File

@@ -0,0 +1,18 @@
package models
// UserListRow — UserList.vue satır modeli (FINAL)
type UserListRow struct {
ID int64 `json:"id"`
Code string `json:"code"`
IsActive bool `json:"is_active"`
// Nebim eşleşmesi (ID yok)
NebimUsername *string `json:"nebim_username,omitempty"`
UserGroupCode *string `json:"user_group_code,omitempty"`
// UIda gösterilecek toplu alanlar
RoleNames string `json:"role_names"` // "ADMIN, USER"
DepartmentNames string `json:"department_names"` // "UST YONETIM, ..."
PiyasaNames string `json:"piyasa_names"` // "AVRUPA, LALELI"
}

20
svc/models/users.go Normal file
View File

@@ -0,0 +1,20 @@
package models
type User struct {
ID int `json:"id"`
Username string `json:"username"`
IsActive bool `json:"is_active"`
Email string `json:"email"`
RoleID int `json:"role_id"`
RoleCode string `json:"role_code"`
FullName string
Mobile string
Address string
V3Username string `json:"v3_username"`
V3UserGroup int `json:"v3_usergroup"`
ForcePasswordChange bool `json:"force_password_change"`
Upass string // 🔐 dfusr.upass (TEK KAYNAK)
}

83
svc/permissions/models.go Normal file
View File

@@ -0,0 +1,83 @@
package permissions
/* =====================================================
ROLE PERMISSION MATRIX (READ)
- Role bazlı matrix ekranı için: role_id + role_code + module/action/allowed
- UI isterse "source" alanı ile satırın nereden geldiğini gösterebilir.
===================================================== */
type PermissionMatrixRow struct {
// Role tarafı
RoleID int `json:"role_id,omitempty"`
RoleCode string `json:"role_code,omitempty"`
// User override tarafı
UserID int64 `json:"user_id,omitempty"`
// Ortak alanlar
Module string `json:"module"`
ModuleCode string `json:"module_code"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
// role | user
Source string `json:"source"`
}
/* =====================================================
ROLE PERMISSION UPDATE (WRITE)
- /api/permissions/matrix POST gibi update endpoint'lerinde kullanılır
===================================================== */
type PermissionUpdateRequest struct {
RoleID int `json:"role_id"`
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
/* =====================================================
USER OVERRIDE MODELS
- mk_sys_user_permissions tablosu için
===================================================== */
// DBden okurken döndüğümüz satır (GET /api/users/{id}/permissions)
type UserOverrideRow struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
// Middleware içinde hızlı okuma için de kullanılabilir (GetUserOverrides)
type UserPermissionOverride struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
// Repo “upsert/insert” gibi operasyonlarda kullanılacak internal model
type UserPermission struct {
UserID int64 `json:"user_id"`
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
// POST payload için tip (SaveUserOverrides input)
type UserPermissionRequest struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
// Role + Department Permission Row
type RoleDepartmentPermission struct {
RoleID int `json:"role_id"`
DepartmentCode string `json:"department_code"`
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
Source string `json:"source"` // "role_dept"
}

View File

@@ -0,0 +1,601 @@
package permissions
import (
"database/sql"
"log"
"strings"
"github.com/lib/pq"
)
type PermissionRepository struct {
DB *sql.DB
}
func NewPermissionRepository(db *sql.DB) *PermissionRepository {
return &PermissionRepository{DB: db}
}
/* =====================================================
MATRIX READ (V2) - ROLE BASED
===================================================== */
func (r *PermissionRepository) GetPermissionMatrixForRoles(
roleIDs []int,
) ([]PermissionMatrixRow, error) {
if len(roleIDs) == 0 {
return []PermissionMatrixRow{}, nil
}
query := `
SELECT
rp.role_id,
rol.code,
rp.module_code,
rp.action,
rp.allowed
FROM mk_sys_role_permissions rp
JOIN dfrole rol ON rol.id = rp.role_id
WHERE rp.role_id = ANY($1)
ORDER BY rol.id, rp.module_code, rp.action
`
rows, err := r.DB.Query(query, pq.Array(roleIDs))
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]PermissionMatrixRow, 0)
for rows.Next() {
var row PermissionMatrixRow
if err := rows.Scan(
&row.RoleID,
&row.RoleCode,
&row.Module,
&row.Action,
&row.Allowed,
); err != nil {
return nil, err
}
row.Source = "role"
list = append(list, row)
}
return list, nil
}
/* =====================================================
MATRIX UPDATE (V2) - ROLE BASED
===================================================== */
func (r *PermissionRepository) UpdatePermissions(
list []PermissionUpdateRequest,
) error {
tx, err := r.DB.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT INTO mk_sys_role_permissions
(role_id, module_code, action, allowed)
VALUES ($1,$2,$3,$4)
ON CONFLICT (role_id, module_code, action)
DO UPDATE SET allowed = EXCLUDED.allowed
`)
if err != nil {
return err
}
defer stmt.Close()
for _, p := range list {
if _, err := stmt.Exec(
p.RoleID,
p.Module,
p.Action,
p.Allowed,
); err != nil {
return err
}
}
return tx.Commit()
}
/* =====================================================
USER OVERRIDES - READ
GET /api/users/{id}/permissions
===================================================== */
// Tek tip: PermissionMatrixRow döndürüyoruz (source=user)
func (r *PermissionRepository) GetUserOverridesByUserID(
userID int64,
) ([]PermissionMatrixRow, error) {
rows, err := r.DB.Query(`
SELECT
user_id,
module_code,
action,
allowed
FROM mk_sys_user_permissions
WHERE user_id = $1
ORDER BY module_code, action
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]PermissionMatrixRow, 0)
for rows.Next() {
var row PermissionMatrixRow
if err := rows.Scan(
&row.UserID,
&row.Module,
&row.Action,
&row.Allowed,
); err != nil {
return nil, err
}
row.Source = "user"
list = append(list, row)
}
return list, nil
}
/* =====================================================
USER OVERRIDES - UPSERT SAVE (typed)
POST /api/users/{id}/permissions
===================================================== */
func (r *PermissionRepository) SaveUserOverrides(
userID int64,
list []UserPermissionRequest,
) error {
log.Println("➡️ REPO SaveUserOverrides START")
log.Println("USER:", userID)
log.Println("ROWS:", len(list))
tx, err := r.DB.Begin()
if err != nil {
log.Println("❌ TX BEGIN ERROR:", err)
return err
}
defer tx.Rollback()
// önce sil
_, err = tx.Exec(`
DELETE FROM mk_sys_user_permissions
WHERE user_id = $1
`, userID)
if err != nil {
log.Println("❌ DELETE ERROR:", err)
return err
}
stmt, err := tx.Prepare(`
INSERT INTO mk_sys_user_permissions
(user_id, module_code, action, allowed)
VALUES ($1,$2,$3,$4)
`)
if err != nil {
log.Println("❌ PREPARE ERROR:", err)
return err
}
defer stmt.Close()
for _, p := range list {
if strings.TrimSpace(p.Module) == "" {
log.Printf("⚠️ SKIP EMPTY MODULE user=%d action=%s",
userID,
p.Action,
)
continue
}
_, err := stmt.Exec(
userID,
p.Module,
p.Action,
p.Allowed,
)
if err != nil {
log.Println("❌ INSERT ERROR:", err)
return err
}
}
if err := tx.Commit(); err != nil {
log.Println("❌ COMMIT ERROR:", err)
return err
}
log.Println("✅ REPO SaveUserOverrides DONE")
return nil
}
/* =====================================================
RESOLUTION HELPERS (middleware için)
===================================================== */
// user override var mı? varsa *bool döner, yoksa nil
func (r *PermissionRepository) HasUserOverride(
userID int64,
module string,
action string,
) (*bool, error) {
var allowed bool
err := r.DB.QueryRow(`
SELECT allowed
FROM mk_sys_user_permissions
WHERE user_id=$1
AND module_code=$2
AND action=$3
`,
userID,
module,
action,
).Scan(&allowed)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &allowed, nil
}
// roleIDs içinden OR logic: herhangi biri allowed=true ise true
func (r *PermissionRepository) HasRoleAccess(
roleIDs []int,
module string,
action string,
) (bool, error) {
if len(roleIDs) == 0 {
return false, nil
}
var count int
err := r.DB.QueryRow(`
SELECT COUNT(*)
FROM mk_sys_role_permissions
WHERE role_id = ANY($1)
AND module_code=$2
AND action=$3
AND allowed=true
`,
pq.Array(roleIDs),
module,
action,
).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
// Final decision: user override varsa onu uygula, yoksa role bazlı
func (r *PermissionRepository) ResolvePermission(
userID int64,
roleIDs []int,
module string,
action string,
) (bool, error) {
override, err := r.HasUserOverride(userID, module, action)
if err != nil {
return false, err
}
if override != nil {
return *override, nil
}
return r.HasRoleAccess(roleIDs, module, action)
}
func (r *PermissionRepository) GetUserOverrides(
userID int64,
) ([]UserPermissionOverride, error) {
rows, err := r.DB.Query(`
SELECT module_code, action, allowed
FROM mk_sys_user_permissions
WHERE user_id = $1
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var list []UserPermissionOverride
for rows.Next() {
var o UserPermissionOverride
if err := rows.Scan(
&o.Module,
&o.Action,
&o.Allowed,
); err != nil {
return nil, err
}
list = append(list, o)
}
return list, nil
}
func (r *PermissionRepository) UpdateUserOverrides(
list []UserPermission,
) error {
tx, err := r.DB.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT INTO mk_sys_user_permissions
(user_id, module_code, action, allowed)
VALUES ($1,$2,$3,$4)
ON CONFLICT (user_id, module_code, action)
DO UPDATE SET allowed = EXCLUDED.allowed
`)
if err != nil {
return err
}
defer stmt.Close()
for _, p := range list {
_, err := stmt.Exec(
p.UserID,
p.Module,
p.Action,
p.Allowed,
)
if err != nil {
return err
}
}
return tx.Commit()
}
func (r *PermissionRepository) ResolveEffectivePermission(
userID int64,
roleID int64,
deptCode string,
module string,
action string,
) (bool, error) {
// 1⃣ USER OVERRIDE
var allowed bool
err := r.DB.QueryRow(`
SELECT allowed
FROM mk_sys_user_permissions
WHERE user_id = $1
AND module_code = $2
AND action = $3
`,
userID, module, action,
).Scan(&allowed)
if err == nil {
return allowed, nil
}
if err != sql.ErrNoRows {
return false, err
}
// ==================================================
// 2⃣ ROLE + DEPARTMENT
// ==================================================
if len(deptCode) > 0 {
var allowed bool
err = r.DB.QueryRow(` -- 🔥 := DEĞİL =
SELECT allowed
FROM vw_role_dept_permissions
WHERE role_id = $1
AND department_code = ANY($2)
AND module_code = $3
AND action = $4
ORDER BY allowed DESC
LIMIT 1
`,
roleID,
pq.Array([]string{deptCode}),
module,
action,
).Scan(&allowed)
if err == nil {
log.Printf(
" ↳ ROLE+DEPT OVERRIDE = %v",
allowed,
)
return allowed, nil
}
if err != sql.ErrNoRows {
log.Println("❌ ROLE+DEPT ERR:", err)
return false, err
}
}
// 3⃣ ROLE DEFAULT
err = r.DB.QueryRow(`
SELECT allowed
FROM mk_sys_role_permissions
WHERE role_id = $1
AND module_code = $2
AND action = $3
`,
roleID, module, action,
).Scan(&allowed)
if err == nil {
return allowed, nil
}
if err != sql.ErrNoRows {
return false, err
}
// 4⃣ DENY
return false, nil
}
func (r *PermissionRepository) ResolvePermissionChain(
userID int64,
roleID int64,
deptCodes []string,
module string,
action string,
) (bool, error) {
log.Printf(
"🔐 PERM CHECK user=%d role=%d dept=%v %s:%s",
userID,
roleID,
deptCodes,
module,
action,
)
// ==================================================
// 1⃣ USER OVERRIDE
// ==================================================
override, err := r.HasUserOverride(userID, module, action)
if err != nil {
log.Println("❌ USER OVERRIDE ERR:", err)
return false, err
}
if override != nil {
log.Printf(
" ↳ USER OVERRIDE = %v",
*override,
)
return *override, nil
}
// ==================================================
// 2⃣ ROLE + DEPARTMENT
// ==================================================
if len(deptCodes) > 0 {
var allowed bool
err := r.DB.QueryRow(`
SELECT allowed
FROM vw_role_dept_permissions
WHERE role_id = $1
AND department_code IN (
SELECT UNNEST($2::text[])
)
AND module_code = $3
AND action = $4
ORDER BY allowed DESC
LIMIT 1
`,
roleID,
pq.Array(deptCodes),
module,
action,
).Scan(&allowed)
if err == nil {
log.Printf(
" ↳ ROLE+DEPT OVERRIDE = %v",
allowed,
)
return allowed, nil
}
if err != sql.ErrNoRows {
log.Println("❌ ROLE+DEPT ERR:", err)
return false, err
}
}
// ==================================================
// 3⃣ ROLE DEFAULT
// ==================================================
var roleAllowed bool
err = r.DB.QueryRow(`
SELECT allowed
FROM mk_sys_role_permissions
WHERE role_id = $1
AND module_code = $2
AND action = $3
`,
roleID,
module,
action,
).Scan(&roleAllowed)
if err == nil {
log.Printf(
" ↳ ROLE DEFAULT = %v",
roleAllowed,
)
return roleAllowed, nil
}
if err != sql.ErrNoRows {
log.Println("❌ ROLE DEFAULT ERR:", err)
return false, err
}
// ==================================================
// 4⃣ DENY
// ==================================================
log.Println(" ↳ NO RULE → DENY")
return false, nil
}

View File

@@ -0,0 +1,123 @@
package permissions
import (
"bssapp-backend/queries"
"database/sql"
"log"
)
type RoleDepartmentPermissionRepo struct {
db *sql.DB
}
func NewRoleDepartmentPermissionRepo(db *sql.DB) *RoleDepartmentPermissionRepo {
return &RoleDepartmentPermissionRepo{db: db}
}
/* ======================================================
GET
====================================================== */
func (r *RoleDepartmentPermissionRepo) Get(
roleID int,
deptCode string,
) ([]RoleDepartmentPermission, error) {
rows, err := r.db.Query(
queries.GetRoleDepartmentPermissions,
roleID,
deptCode,
)
if err != nil {
return nil, err
}
defer rows.Close()
var list []RoleDepartmentPermission
for rows.Next() {
var p RoleDepartmentPermission
if err := rows.Scan(
&p.Module,
&p.Action,
&p.Allowed,
); err != nil {
return nil, err
}
p.RoleID = roleID
p.DepartmentCode = deptCode
p.Source = "role_department"
list = append(list, p)
}
return list, nil
}
/* ======================================================
SAVE
====================================================== */
func (r *RoleDepartmentPermissionRepo) Save(
roleID int,
deptCode string,
list []RoleDepartmentPermission,
) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(
queries.UpsertRoleDepartmentPermission,
)
if err != nil {
return err
}
defer stmt.Close()
for _, p := range list {
if p.Module == "" {
log.Printf(
"⚠️ SKIP EMPTY MODULE role=%v dept=%v action=%v",
roleID,
deptCode,
p.Action,
)
continue
}
_, err := stmt.Exec(
roleID,
deptCode,
p.Module,
p.Action,
p.Allowed,
)
// 🔴🔴🔴 KRİTİK DEBUG LOG
if err != nil {
log.Printf(
"❌ UPSERT FAIL role=%v dept=%v module=%v action=%v allowed=%v err=%v",
roleID,
deptCode,
p.Module,
p.Action,
p.Allowed,
err,
)
return err
}
}
return tx.Commit()
}

View File

@@ -0,0 +1,33 @@
package permissions
// 🔥 SYSTEM / ADMIN ROLE IDS
var AdminRoleIDs = map[int]struct{}{
1: {},
3: {},
}
// -------------------------------------------------------
// 🧠 ROLE POLICY
// -------------------------------------------------------
func ResolveEffectiveRoles(
roleIDs []int,
) (effectiveRoleIDs []int, isAdmin bool) {
roleMap := make(map[int]struct{})
for _, roleID := range roleIDs {
// 🔥 SYSTEM / ADMIN OVERRIDE (1,3,…)
if _, ok := AdminRoleIDs[roleID]; ok {
return []int{roleID}, true
}
roleMap[roleID] = struct{}{}
}
for id := range roleMap {
effectiveRoleIDs = append(effectiveRoleIDs, id)
}
return effectiveRoleIDs, false
}

40
svc/permissions/seed.go Normal file
View File

@@ -0,0 +1,40 @@
package permissions
import "database/sql"
func SeedAdminRoleDepartments(db *sql.DB) error {
var adminID int
var err error
// Admin role id al
err = db.QueryRow(`
SELECT id
FROM dfrole
WHERE code = 'admin'
`).Scan(&adminID)
if err != nil {
return err
}
// Seed
_, err = db.Exec(`
INSERT INTO mk_sys_role_department_permissions
(role_id, department_code, module_code, action, allowed)
SELECT
$1,
d.code,
r.module_code,
r.action,
true
FROM mk_dprt d
CROSS JOIN mk_sys_routes r
ON CONFLICT (role_id, department_code, module_code, action)
DO NOTHING
`, adminID)
return err
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

74
svc/queries/account.go Normal file
View File

@@ -0,0 +1,74 @@
package queries
import (
"context"
"fmt"
"log"
"strings"
"bssapp-backend/db"
"bssapp-backend/internal/authz"
"bssapp-backend/models"
)
func GetAccounts(ctx context.Context) ([]models.Account, error) {
piyasaFilter := authz.BuildMSSQLPiyasaFilter(ctx, "f2.CustomerAtt01")
if strings.TrimSpace(piyasaFilter) == "" {
piyasaFilter = "1=1"
}
query := fmt.Sprintf(`
SELECT
x.AccountCode,
MAX(x.AccountName) AS AccountName
FROM (
SELECT
LEFT(b.CurrAccCode, 8) AS AccountCode,
COALESCE(d.CurrAccDescription, '') AS AccountName
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON d.CurrAccCode = b.CurrAccCode
JOIN CustomerAttributesFilter f2
ON f2.CurrAccCode = b.CurrAccCode
WHERE %s
) x
GROUP BY x.AccountCode
ORDER BY x.AccountCode
`, piyasaFilter)
log.Println("🔎 ACCOUNT PIYASA FILTER =", piyasaFilter)
log.Println("🔎 ACCOUNT QUERY =", query)
rows, err := db.MssqlDB.Query(query)
if err != nil {
return nil, fmt.Errorf("MSSQL query error: %w", err)
}
defer rows.Close()
var accounts []models.Account
for rows.Next() {
var acc models.Account
if err := rows.Scan(
&acc.AccountCode,
&acc.AccountName,
); err != nil {
return nil, err
}
if len(acc.AccountCode) >= 4 {
acc.DisplayCode =
strings.TrimSpace(acc.AccountCode[:3] + " " + acc.AccountCode[3:])
} else {
acc.DisplayCode = acc.AccountCode
}
accounts = append(accounts, acc)
}
return accounts, rows.Err()
}

View File

@@ -0,0 +1,64 @@
package queries
import (
"bssapp-backend/models"
"database/sql"
"sync"
"time"
)
/* ===============================
CACHE STRUCT
================================ */
type currencyCacheItem struct {
data *models.TodayCurrencyV3
expiresAt time.Time
}
var (
currencyCache = make(map[string]currencyCacheItem)
cacheMutex sync.RWMutex
cacheTTL = 5 * time.Minute
)
/* ===============================
MAIN CACHE FUNC
================================ */
func GetCachedCurrencyV3(db *sql.DB, code string) (*models.TodayCurrencyV3, error) {
now := time.Now()
/* ---------- READ CACHE ---------- */
cacheMutex.RLock()
item, ok := currencyCache[code]
if ok && now.Before(item.expiresAt) {
cacheMutex.RUnlock()
return item.data, nil
}
cacheMutex.RUnlock()
/* ---------- FETCH DB ---------- */
data, err := GetTodayCurrencyV3(db, code)
if err != nil {
return nil, err
}
/* ---------- WRITE CACHE ---------- */
cacheMutex.Lock()
currencyCache[code] = currencyCacheItem{
data: data,
expiresAt: now.Add(cacheTTL),
}
cacheMutex.Unlock()
return data, nil
}

114
svc/queries/customerlist.go Normal file
View File

@@ -0,0 +1,114 @@
package queries
import (
"context"
"fmt"
"bssapp-backend/db"
"bssapp-backend/internal/authz"
"bssapp-backend/models"
)
func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) {
piyasaFilter := authz.BuildMSSQLPiyasaFilter(
ctx,
"f.CustomerAtt01",
)
query := fmt.Sprintf(`
SELECT
c.CurrAccTypeCode,
c.CurrAccCode,
ISNULL(d.CurrAccDescription, ''),
dbo.HG_Temizlik(
ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 8
AND AttributeCode = f.CustomerAtt08
AND LangCode = 'TR'
), SPACE(0))
),
dbo.HG_Temizlik(
ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 1
AND AttributeCode = f.CustomerAtt01
AND LangCode = 'TR'
), SPACE(0))
),
dbo.HG_Temizlik(
ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 2
AND AttributeCode = f.CustomerAtt02
AND LangCode = 'TR'
), SPACE(0))
),
dbo.HG_Temizlik(
ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 5
AND AttributeCode = f.CustomerAtt05
AND LangCode = 'TR'
), SPACE(0))
),
ISNULL(c.CurrencyCode, '')
FROM cdCurrAcc c
LEFT JOIN cdCurrAccDesc d
ON c.CurrAccCode = d.CurrAccCode
LEFT JOIN CustomerAttributesFilter f
ON c.CurrAccCode = f.CurrAccCode
WHERE
c.CompanyCode = 1
AND c.CurrAccTypeCode = 3
AND c.IsBlocked = 0
AND %s
ORDER BY d.CurrAccDescription
`, piyasaFilter)
rows, err := db.MssqlDB.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var list []models.CustomerList
for rows.Next() {
var c models.CustomerList
if err := rows.Scan(
&c.CurrAccTypeCode,
&c.Cari_Kod,
&c.Cari_Ad,
&c.Musteri_Ana_Grubu,
&c.Piyasa,
&c.Musteri_Temsilcisi,
&c.Ulke,
&c.Doviz_cinsi,
); err != nil {
return nil, err
}
list = append(list, c)
}
return list, rows.Err()
}

View File

@@ -0,0 +1,52 @@
package queries
import (
"database/sql"
"fmt"
)
func GetOrderListExcel(
db *sql.DB,
search string,
currAcc string,
orderDate string,
) (*sql.Rows, error) {
q := OrderListBaseQuery + " AND 1=1 "
args := []interface{}{}
// SEARCH
if search != "" {
q += `
AND (
LOWER(h.OrderNumber) LIKE LOWER(@p1) OR
LOWER(h.CurrAccCode) LIKE LOWER(@p1) OR
LOWER(ca.CurrAccDescription) LIKE LOWER(@p1) OR
LOWER(h.Description) LIKE LOWER(@p1) OR
LOWER(mt.AttributeDescription) LIKE LOWER(@p1) OR
LOWER(py.AttributeDescription) LIKE LOWER(@p1)
)
`
args = append(args, "%"+search+"%")
}
// CURRACC
if currAcc != "" {
q += fmt.Sprintf(" AND h.CurrAccCode = @p%d ", len(args)+1)
args = append(args, currAcc)
}
// DATE
if orderDate != "" {
q += fmt.Sprintf(
" AND CONVERT(varchar, h.OrderDate, 23) = @p%d ",
len(args)+1,
)
args = append(args, orderDate)
}
// ORDER BY SONDA
q += " ORDER BY h.CreatedDate DESC "
return db.Query(q, args...)
}

174
svc/queries/helpers.go Normal file
View File

@@ -0,0 +1,174 @@
package queries
import (
"bssapp-backend/models"
"database/sql"
"fmt"
"github.com/google/uuid"
"log"
"strings"
"time"
)
// ============================================================
// 🔥 UNIVERSAL DATETIME PARSER
// ============================================================
func parseDateTime(str string) (time.Time, error) {
layouts := []string{
"2006-01-02 15:04:05", // "2025-11-21 12:37:21"
time.RFC3339, // "2025-11-21T12:37:21Z"
"2006-01-02", // "2025-11-21"
}
for _, layout := range layouts {
if t, err := time.Parse(layout, str); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("datetime parse edilemedi: %s", str)
}
// ============================================================
// 📌 DATE (YYYY-MM-DD) → sql.NullTime
// ============================================================
func nullableDateString(ns models.NullString) sql.NullTime {
if ns.Valid && strings.TrimSpace(ns.String) != "" {
t, err := time.Parse("2006-01-02", ns.String)
if err == nil {
return sql.NullTime{Valid: true, Time: t}
}
}
return sql.NullTime{}
}
// ============================================================
// 📌 TIME (HH:mm:ss) → sql.NullString
// ============================================================
func nullableTimeString(ns models.NullString) sql.NullString {
if ns.Valid && strings.TrimSpace(ns.String) != "" {
return sql.NullString{String: ns.String, Valid: true}
}
return sql.NullString{}
}
// ============================================================
// 📌 DATETIME (CustomTime → sql.NullTime)
// ============================================================
func nullableDateTime(ct models.CustomTime, fallback time.Time) sql.NullTime {
if ct.Valid && !ct.Time.IsZero() {
return sql.NullTime{Valid: true, Time: ct.Time}
}
return sql.NullTime{Valid: true, Time: fallback}
}
// ============================================================
// 📌 DATETIME (NullTime → sql.NullTime)
// ============================================================
func nullableTime(nt models.NullTime, fallback time.Time) sql.NullTime {
if nt.Valid && !nt.Time.IsZero() {
return sql.NullTime{Valid: true, Time: nt.Time}
}
return sql.NullTime{Valid: true, Time: fallback}
}
// ============================================================
// 📌 DATETIME (NullString → sql.NullTime)
// ============================================================
func nullableDateTimeString(ns models.NullString, fallback time.Time) sql.NullTime {
if !ns.Valid || strings.TrimSpace(ns.String) == "" {
return sql.NullTime{Time: fallback, Valid: true}
}
t, err := parseDateTime(ns.String)
if err != nil {
return sql.NullTime{Time: fallback, Valid: true}
}
return sql.NullTime{Time: t, Valid: true}
}
// ============================================================
// 📌 STRING → sql.NullString
// ============================================================
func nullableString(val models.NullString, fallback string) sql.NullString {
if val.Valid && strings.TrimSpace(val.String) != "" {
return sql.NullString{String: val.String, Valid: true}
}
return sql.NullString{String: fallback, Valid: true}
}
// ============================================================
// 📌 GUID (UNIQUEIDENTIFIER) → interface{}
// ============================================================
func nullableUUID(v interface{}) interface{} {
switch x := v.(type) {
// 1⃣ models.NullUUID → direkt geri dön
case models.NullUUID:
if !x.Valid {
return nil
}
return x.UUID
// 2⃣ models.NullString → GUID parse et
case models.NullString:
if !x.Valid || strings.TrimSpace(x.String) == "" {
return nil
}
id, err := uuid.Parse(x.String)
if err != nil {
return nil
}
return id
// 3⃣ string → GUID parse et
case string:
id, err := uuid.Parse(x)
if err != nil {
return nil
}
return id
}
return nil
}
// ============================================================
// 📌 NUMERIC → sql.NullX
// ============================================================
func nullableFloat64(val models.NullFloat64, fallback float64) sql.NullFloat64 {
if val.Valid {
return sql.NullFloat64{Float64: val.Float64, Valid: true}
}
return sql.NullFloat64{Float64: fallback, Valid: true}
}
func nullableInt16(val models.NullInt16, fallback int16) sql.NullInt16 {
if val.Valid {
return sql.NullInt16{Int16: val.Int16, Valid: true}
}
return sql.NullInt16{Int16: fallback, Valid: true}
}
func nullableInt32(val models.NullInt32, fallback int32) sql.NullInt32 {
if val.Valid {
return sql.NullInt32{Int32: val.Int32, Valid: true}
}
return sql.NullInt32{Int32: fallback, Valid: true}
}
func nullableInt32ToInt16(val models.NullInt32, fallback int16) sql.NullInt16 {
if val.Valid {
return sql.NullInt16{Int16: int16(val.Int32), Valid: true}
}
return sql.NullInt16{Int16: fallback, Valid: true}
}
// ============================================================
// 📌 BOOL → sql.NullBool
// ============================================================
func nullableBool(val models.NullBool, fallback bool) sql.NullBool {
if val.Valid {
return sql.NullBool{Bool: val.Bool, Valid: true}
}
return sql.NullBool{Bool: fallback, Valid: true}
}
var logger = log.Default()

View File

@@ -0,0 +1,93 @@
package queries
// 🔹 Ürüne göre stok detay sorgusu
const GetInventoryProduct = `
-- 🔹 Ürüne göre stok detay sorgusu (Nebim V3 uyumlu, parametre güvenli)
SELECT
bsItemTypeDesc.ItemTypeDescription AS InventoryType,
inv.WarehouseCode AS Depo_Kodu,
wh.WarehouseDescription AS Depo_Adi,
inv.ItemCode AS Urun_Kodu,
ISNULL(descItem.ItemDescription, '') AS Madde_Aciklamasi,
inv.ColorCode AS Renk_Kodu,
ISNULL(descColor.ColorDescription, '') AS Renk_Aciklamasi,
inv.ItemDim1Code AS Beden,
attr.ProductAtt01 AS Urun_Grubu,
attr.ProductAtt02 AS Urun_Alt_Grubu,
attr.ProductAtt41 AS Kisa_Karisim,
attr.ProductAtt42 AS SERI,
attr.ProductAtt43 AS FASON_ISCLIK,
attr.ProductAtt45 AS ASKILI_YAN,
inv.ItemDim2Code AS YAKA,
attr.ProductAtt44 AS GARSON_YETISKIN,
attr.ProductAtt10 AS MarkaKodu,
ROUND(
SUM(inv.InventoryQty1)
- (SUM(inv.ReserveQty1) + SUM(inv.DispOrderQty1) + SUM(inv.PickingQty1)),
cdUnitOfMeasure.RoundDigit
) AS Kullanilabilir_Envanter
FROM cdItem WITH (NOLOCK)
JOIN cdUnitOfMeasure WITH (NOLOCK)
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
JOIN (
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(CASE WHEN SourceTable = 'PickingStates' THEN Qty1 ELSE 0 END) AS PickingQty1,
SUM(CASE WHEN SourceTable = 'ReserveStates' THEN Qty1 ELSE 0 END) AS ReserveQty1,
SUM(CASE WHEN SourceTable = 'DispOrderStates' THEN Qty1 ELSE 0 END) AS DispOrderQty1,
SUM(CASE WHEN SourceTable = 'trStock' THEN (In_Qty1 - Out_Qty1) ELSE 0 END) AS InventoryQty1
FROM (
SELECT 'PickingStates' AS SourceTable, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0 AS In_Qty1, 0 AS Out_Qty1
FROM PickingStates WITH (NOLOCK)
UNION ALL
SELECT 'ReserveStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0
FROM ReserveStates WITH (NOLOCK)
UNION ALL
SELECT 'DispOrderStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0
FROM DispOrderStates WITH (NOLOCK)
UNION ALL
SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, 0, SUM(In_Qty1), SUM(Out_Qty1)
FROM trStock WITH (NOLOCK)
GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) AS src
GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) AS inv
ON cdItem.ItemTypeCode = inv.ItemTypeCode
AND cdItem.ItemCode = inv.ItemCode
LEFT JOIN ProductAttributesFilter AS attr WITH (NOLOCK)
ON attr.ItemCode = inv.ItemCode
LEFT JOIN bsItemTypeDesc WITH (NOLOCK)
ON bsItemTypeDesc.ItemTypeCode = inv.ItemTypeCode
AND bsItemTypeDesc.LangCode = 'TR'
LEFT JOIN cdWarehouseDesc AS wh WITH (NOLOCK)
ON wh.WarehouseCode = inv.WarehouseCode
LEFT JOIN cdItemDesc AS descItem WITH (NOLOCK)
ON descItem.ItemTypeCode = inv.ItemTypeCode
AND descItem.ItemCode = inv.ItemCode
AND descItem.LangCode = 'TR'
LEFT JOIN cdColorDesc AS descColor WITH (NOLOCK)
ON descColor.ColorCode = inv.ColorCode
AND descColor.LangCode = 'TR'
WHERE
inv.ItemTypeCode IN (1)
AND inv.WarehouseCode IN ('1-0-12','1-0-21','1-0-10','1-0-2','1-1-3','1-2-4','1-2-5','')
AND inv.InventoryQty1 >= 0
AND cdItem.IsBlocked = 0
AND inv.ItemCode = @p1 -- ✅ doğrudan parametre, DECLARE yok
GROUP BY
inv.CompanyCode, inv.OfficeCode, inv.StoreTypeCode, inv.StoreCode,
inv.WarehouseCode, inv.ItemTypeCode, inv.ItemCode, inv.ColorCode,
inv.ItemDim1Code, inv.ItemDim2Code, inv.ItemDim3Code,
cdUnitOfMeasure.RoundDigit, attr.ProductAtt01, attr.ProductAtt02,
attr.ProductAtt41, attr.ProductAtt42, attr.ProductAtt43, attr.ProductAtt44,
attr.ProductAtt45, attr.ProductAtt10, bsItemTypeDesc.ItemTypeDescription,
wh.WarehouseDescription, descItem.ItemDescription, descColor.ColorDescription;
`

280
svc/queries/order_get.go Normal file
View File

@@ -0,0 +1,280 @@
package queries
import (
"bssapp-backend/db"
"bssapp-backend/models"
"database/sql"
"errors"
"fmt"
)
// GetOrderByID — Sipariş başlığı (header) ve satırlarını (lines) getirir.
func GetOrderByID(orderID string) (*models.OrderHeader, []models.OrderDetail, error) {
conn := db.GetDB()
logger.Printf("🧾 [GetOrderByID] begin • id=%s", orderID)
// =====================================================
// HEADER (Cari adı join'li)
// =====================================================
var header models.OrderHeader
qHeader := `
SELECT
CAST(h.OrderHeaderID AS varchar(36)) AS OrderHeaderID,
h.OrderTypeCode,
h.ProcessCode,
h.OrderNumber,
h.IsCancelOrder,
h.OrderDate,
h.OrderTime,
h.DocumentNumber,
h.PaymentTerm,
h.AverageDueDate,
h.Description,
h.InternalDescription,
h.CurrAccTypeCode,
h.CurrAccCode,
d.CurrAccDescription,
h.SubCurrAccID,
h.ContactID,
h.ShipmentMethodCode,
h.ShippingPostalAddressID,
h.BillingPostalAddressID,
h.GuarantorContactID,
h.GuarantorContactID2,
h.RoundsmanCode,
h.DeliveryCompanyCode,
h.TaxTypeCode,
h.WithHoldingTaxTypeCode,
h.DOVCode,
h.TaxExemptionCode,
h.CompanyCode,
h.OfficeCode,
h.StoreTypeCode,
h.StoreCode,
h.POSTerminalID,
h.WarehouseCode,
h.ToWarehouseCode,
h.OrdererCompanyCode,
h.OrdererOfficeCode,
h.OrdererStoreCode,
h.GLTypeCode,
h.DocCurrencyCode,
h.LocalCurrencyCode,
h.ExchangeRate,
h.TDisRate1,
h.TDisRate2,
h.TDisRate3,
h.TDisRate4,
h.TDisRate5,
h.DiscountReasonCode,
h.SurplusOrderQtyToleranceRate,
h.ImportFileNumber,
h.ExportFileNumber,
h.IncotermCode1,
h.IncotermCode2,
h.LettersOfCreditNumber,
h.PaymentMethodCode,
h.IsInclutedVat,
h.IsCreditSale,
h.IsCreditableConfirmed,
h.CreditableConfirmedUser,
h.CreditableConfirmedDate,
h.IsSalesViaInternet,
h.IsSuspended,
h.IsCompleted,
h.IsPrinted,
h.IsLocked,
h.UserLocked,
h.IsClosed,
h.ApplicationCode,
h.ApplicationID,
h.CreatedUserName,
h.CreatedDate,
h.LastUpdatedUserName,
h.LastUpdatedDate,
h.IsProposalBased
FROM BAGGI_V3.dbo.trOrderHeader AS h
LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d
ON h.CurrAccCode = d.CurrAccCode
WHERE h.OrderHeaderID = @p1;
`
err := conn.QueryRow(qHeader, orderID).Scan(
&header.OrderHeaderID,
&header.OrderTypeCode,
&header.ProcessCode,
&header.OrderNumber,
&header.IsCancelOrder,
&header.OrderDate,
&header.OrderTime,
&header.DocumentNumber,
&header.PaymentTerm,
&header.AverageDueDate,
&header.Description,
&header.InternalDescription,
&header.CurrAccTypeCode,
&header.CurrAccCode,
&header.CurrAccDescription,
&header.SubCurrAccID,
&header.ContactID,
&header.ShipmentMethodCode,
&header.ShippingPostalAddressID,
&header.BillingPostalAddressID,
&header.GuarantorContactID,
&header.GuarantorContactID2,
&header.RoundsmanCode,
&header.DeliveryCompanyCode,
&header.TaxTypeCode,
&header.WithHoldingTaxTypeCode,
&header.DOVCode,
&header.TaxExemptionCode,
&header.CompanyCode,
&header.OfficeCode,
&header.StoreTypeCode,
&header.StoreCode,
&header.POSTerminalID,
&header.WarehouseCode,
&header.ToWarehouseCode,
&header.OrdererCompanyCode,
&header.OrdererOfficeCode,
&header.OrdererStoreCode,
&header.GLTypeCode,
&header.DocCurrencyCode,
&header.LocalCurrencyCode,
&header.ExchangeRate,
&header.TDisRate1,
&header.TDisRate2,
&header.TDisRate3,
&header.TDisRate4,
&header.TDisRate5,
&header.DiscountReasonCode,
&header.SurplusOrderQtyToleranceRate,
&header.ImportFileNumber,
&header.ExportFileNumber,
&header.IncotermCode1,
&header.IncotermCode2,
&header.LettersOfCreditNumber,
&header.PaymentMethodCode,
&header.IsInclutedVat,
&header.IsCreditSale,
&header.IsCreditableConfirmed,
&header.CreditableConfirmedUser,
&header.CreditableConfirmedDate,
&header.IsSalesViaInternet,
&header.IsSuspended,
&header.IsCompleted,
&header.IsPrinted,
&header.IsLocked,
&header.UserLocked,
&header.IsClosed,
&header.ApplicationCode,
&header.ApplicationID,
&header.CreatedUserName,
&header.CreatedDate,
&header.LastUpdatedUserName,
&header.LastUpdatedDate,
&header.IsProposalBased,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
logger.Printf("⚠️ [GetOrderByID] sipariş bulunamadı: %s", orderID)
return nil, nil, sql.ErrNoRows
}
logger.Printf("❌ [GetOrderByID] header sorgu hatası: %v", err)
return nil, nil, err
}
logger.Printf("✅ [GetOrderByID] header loaded • orderNo=%v currAcc=%v",
header.OrderNumber, header.CurrAccCode.String)
// =====================================================
// LINES
// =====================================================
qLines := `
SELECT
CAST(L.OrderLineID AS varchar(36)) AS OrderLineID,
L.SortOrder,
L.ItemTypeCode,
L.ItemCode,
L.ColorCode,
L.ItemDim1Code,
L.ItemDim2Code,
L.ItemDim3Code,
L.Qty1,
L.Qty2,
L.Price,
L.VatRate,
L.PCTRate,
L.DocCurrencyCode,
L.DeliveryDate,
L.PlannedDateOfLading,
L.LineDescription,
L.IsClosed,
L.CreatedUserName,
L.CreatedDate,
L.LastUpdatedUserName,
L.LastUpdatedDate,
P.ProductAtt42Desc AS UrunIlkGrubu,
P.ProductAtt01Desc AS UrunAnaGrubu,
P.ProductAtt02Desc AS UrunAltGrubu,
P.ProductAtt38Desc AS Fit1,
P.ProductAtt39Desc AS Fit2
FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN ProductFilterWithDescription('TR') AS P
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
WHERE L.OrderHeaderID = @p1
ORDER BY L.SortOrder ASC;
`
rows, err := conn.Query(qLines, orderID)
if err != nil {
logger.Printf("❌ [GetOrderByID] line sorgu hatası: %v", err)
return &header, nil, err
}
defer rows.Close()
lines := make([]models.OrderDetail, 0, 32)
for rows.Next() {
var ln models.OrderDetail
if err := rows.Scan(
&ln.OrderLineID,
&ln.SortOrder,
&ln.ItemTypeCode,
&ln.ItemCode,
&ln.ColorCode,
&ln.ItemDim1Code,
&ln.ItemDim2Code,
&ln.ItemDim3Code,
&ln.Qty1,
&ln.Qty2,
&ln.Price,
&ln.VatRate,
&ln.PCTRate,
&ln.DocCurrencyCode,
&ln.DeliveryDate,
&ln.PlannedDateOfLading,
&ln.LineDescription,
&ln.IsClosed,
&ln.CreatedUserName,
&ln.CreatedDate,
&ln.LastUpdatedUserName,
&ln.LastUpdatedDate,
&ln.UrunIlkGrubu,
&ln.UrunAnaGrubu,
&ln.UrunAltGrubu,
&ln.Fit1,
&ln.Fit2,
); err != nil {
return &header, nil, fmt.Errorf("line scan hatası: %w", err)
}
lines = append(lines, ln)
}
if err := rows.Err(); err != nil {
return &header, nil, fmt.Errorf("line rows hatası: %w", err)
}
logger.Printf("📦 [GetOrderByID] lines loaded • count=%d", len(lines))
return &header, lines, nil
}

161
svc/queries/order_pdf.go Normal file
View File

@@ -0,0 +1,161 @@
package queries
import (
"context"
"database/sql"
)
/*
============================================================
HEADER GETIRME — OrderHeader structına uygun
============================================================
*/
type OrderHeaderDB struct {
OrderHeaderID string
OrderNumber string
CurrAccCode string
CurrAccName string
DocCurrency string
OrderDate sql.NullTime
Description sql.NullString
InternalDesc sql.NullString
OfficeCode sql.NullString
CreatedUser sql.NullString
}
func GetOrderHeaderDB(ctx context.Context, db *sql.DB, id string) (*OrderHeaderDB, error) {
q := `
SELECT
CAST(h.OrderHeaderID AS varchar(36)),
h.OrderNumber,
h.CurrAccCode,
d.CurrAccDescription,
h.DocCurrencyCode,
h.OrderDate,
h.Description,
h.InternalDescription,
h.OfficeCode,
h.CreatedUserName
FROM BAGGI_V3.dbo.trOrderHeader AS h
LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d
ON h.CurrAccCode = d.CurrAccCode
WHERE h.OrderHeaderID = @p1
`
row := db.QueryRowContext(ctx, q, id)
var h OrderHeaderDB
err := row.Scan(
&h.OrderHeaderID,
&h.OrderNumber,
&h.CurrAccCode,
&h.CurrAccName,
&h.DocCurrency,
&h.OrderDate,
&h.Description,
&h.InternalDesc,
&h.OfficeCode,
&h.CreatedUser,
)
if err != nil {
return nil, err
}
return &h, nil
}
/*
============================================================
SATIRLARI GETIRME — OrderLineRaw structına uygun
============================================================
*/
type OrderLineRawDB struct {
OrderLineID sql.NullString
ItemCode string
ColorCode string
ItemDim1Code sql.NullString
ItemDim2Code sql.NullString
Qty1 sql.NullFloat64
Price sql.NullFloat64
DocCurrencyCode sql.NullString
DeliveryDate sql.NullTime
LineDescription sql.NullString
UrunAnaGrubu sql.NullString
UrunAltGrubu sql.NullString
IsClosed sql.NullBool
WithHoldingTaxType sql.NullString
DOVCode sql.NullString
PlannedDateOfLading sql.NullTime
CostCenterCode sql.NullString
VatCode sql.NullString
VatRate sql.NullFloat64
}
func GetOrderLinesDB(ctx context.Context, db *sql.DB, id string) ([]OrderLineRawDB, error) {
q := `
SELECT
CAST(L.OrderLineID AS varchar(36)),
L.ItemCode,
L.ColorCode,
L.ItemDim1Code,
L.ItemDim2Code,
L.Qty1,
L.Price,
L.DocCurrencyCode,
L.DeliveryDate,
L.LineDescription,
P.ProductAtt01Desc,
P.ProductAtt02Desc,
L.IsClosed,
L.WithHoldingTaxTypeCode,
L.DOVCode,
L.PlannedDateOfLading,
L.CostCenterCode,
L.VatCode,
L.VatRate
FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN ProductFilterWithDescription('TR') AS P
ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
WHERE L.OrderHeaderID = @p1
ORDER BY L.SortOrder, L.OrderLineID
`
rows, err := db.QueryContext(ctx, q, id)
if err != nil {
return nil, err
}
defer rows.Close()
var out []OrderLineRawDB
for rows.Next() {
var r OrderLineRawDB
err = rows.Scan(
&r.OrderLineID,
&r.ItemCode,
&r.ColorCode,
&r.ItemDim1Code,
&r.ItemDim2Code,
&r.Qty1,
&r.Price,
&r.DocCurrencyCode,
&r.DeliveryDate,
&r.LineDescription,
&r.UrunAnaGrubu,
&r.UrunAltGrubu,
&r.IsClosed,
&r.WithHoldingTaxType,
&r.DOVCode,
&r.PlannedDateOfLading,
&r.CostCenterCode,
&r.VatCode,
&r.VatRate,
)
if err != nil {
return nil, err
}
out = append(out, r)
}
return out, nil
}

1079
svc/queries/order_write.go Normal file
View File

@@ -0,0 +1,1079 @@
// ===================== PART 1 (Satır 1-300) =====================
// =======================================================
// order_write.go — v4.3 FINAL (Insert + Update + Delete)
// - PCTCode her zaman "%0" olarak yazılır (cdPCT FK uyumlu)
// - INSERT/UPDATE öncesi ItemVariant Guard + Duplicate Guard (payload içi)
// - UpdateOrder: DELETE öncesi child tablolar (trOrderLineCurrency) temizlenir
// =======================================================
package queries
import (
"bssapp-backend/db"
"bssapp-backend/models"
"database/sql"
"fmt"
"github.com/google/uuid"
"strings"
"time"
)
func nf0(v models.NullFloat64) float64 {
if !v.Valid {
return 0
}
return v.Float64
}
// =======================================================
// COMBO KEY & STRING HELPERS
// =======================================================
func normalizeComboKey(s string) string {
return strings.ToUpper(strings.TrimSpace(s))
}
// makeComboKey: frontend tarafında NullString olmayan structlar için
func makeComboKey(ln models.OrderDetail) string {
model := safeNS(ln.ItemCode)
renk := safeNS(ln.ColorCode)
renk2 := safeNS(ln.ItemDim2Code)
beden := safeNS(ln.ItemDim1Code)
return normalizeComboKey(
fmt.Sprintf("%s||%s||%s||%s", model, renk, beden, renk2),
)
}
// qtyValue → NullFloat64 güvenli float64
func qtyValue(q models.NullFloat64) float64 {
if !q.Valid {
return 0
}
return q.Float64
}
// VatCode: NullString → string
// - NULL → ""
// - "0" → "" (FK patlamasın, sadece anlamlı kodlar gönderiyoruz)
// - "%0", "%10" vb → trimlenmiş hali
func normalizeVatCode(ns models.NullString) string {
if !ns.Valid {
return ""
}
s := strings.TrimSpace(ns.String)
if s == "0" {
return ""
}
return s
}
// PCTCode: NullString → string
// - Artık her durumda "%0" döndürür (tek PCT tipi kullanımı)
// NOT: SQL tarafında da "%0" cdPCT içinde tanımlı olmalı
func normalizePCTCode(ns models.NullString) string {
return "%0"
}
// models.NullString → trimlenmiş string (NULL ise "")
func safeNS(ns models.NullString) string {
if !ns.Valid {
return ""
}
return strings.TrimSpace(ns.String)
}
// =======================================================
// COMBO KEY HELPERS (DB alanlarından)
// =======================================================
// makeComboKeyParts: düz string alanlardan comboKey üret
// comboKey = model || renk || beden || renk2
func makeComboKeyParts(item, color, dim1, dim2 string) string {
item = strings.TrimSpace(item)
color = strings.TrimSpace(color)
dim1 = strings.TrimSpace(dim1)
dim2 = strings.TrimSpace(dim2)
if item == "" && color == "" && dim1 == "" && dim2 == "" {
return ""
}
return normalizeComboKey(item + "||" + color + "||" + dim1 + "||" + dim2)
}
// comboFromNulls: NullString alanlardan comboKey üret
func comboFromNulls(item, color, dim1, dim2 models.NullString) string {
return makeComboKeyParts(
safeNS(item),
safeNS(color),
safeNS(dim1),
safeNS(dim2),
)
}
// =======================================================
// ✅ ItemVariant Guard — INSERT / UPDATE öncesi
// =======================================================
// normalizeKeyPart: NullString → trim + UPPER
func normalizeKeyPart(ns models.NullString) string {
s := strings.TrimSpace(safeNS(ns))
return strings.ToUpper(s)
}
// =======================================================
// AKSBIR DETECTION
// =======================================================
// =======================================================
// COMBO KEY BUILDER (AKSBIR AWARE)
// =======================================================
// Variant check: ItemCode + ColorCode + Dim1 + Dim2
func ValidateItemVariant(tx *sql.Tx, ln models.OrderDetail) error {
fmt.Printf(
"🧪 VARIANT GUARD INPUT | ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
safeNS(ln.ClientKey),
safeNS(ln.ItemCode),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
safeNS(ln.ItemDim3Code),
nf0(ln.Qty1),
)
item := normalizeKeyPart(ln.ItemCode)
color := normalizeKeyPart(ln.ColorCode)
dim1 := normalizeKeyPart(ln.ItemDim1Code)
dim2 := normalizeKeyPart(ln.ItemDim2Code)
// ✅ Placeholder/boş standardizasyon (SENDE "_" geliyor)
normalizeEmpty := func(s string) string {
s = strings.TrimSpace(strings.ToUpper(s))
if s == "_" || s == "-" {
return ""
}
return s
}
item = normalizeEmpty(item)
color = normalizeEmpty(color)
dim1 = normalizeEmpty(dim1)
dim2 = normalizeEmpty(dim2)
if item == "" {
return fmt.Errorf(
"ItemCode boş olamaz (ClientKey=%s)",
safeNS(ln.ClientKey),
)
fmt.Printf(
"🧪 VARIANT NORMALIZED | Item=%q Color=%q Dim1=%q Dim2=%q\n",
item, color, dim1, dim2,
)
}
// İstersen debug:
// fmt.Printf("🧪 VARIANT CHECK item=%q color=%q dim1=%q dim2=%q clientKey=%s\n", item, color, dim1, dim2, safeNS(ln.ClientKey))
var exists int
err := tx.QueryRow(`
SELECT CASE WHEN EXISTS (
SELECT 1
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK)
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3
AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4
) THEN 1 ELSE 0 END
`, item, color, dim1, dim2).Scan(&exists)
if err != nil {
return fmt.Errorf("ItemVariant kontrol query hatası: %w", err)
}
if exists != 1 {
return &models.ValidationError{
Code: "INVALID_ITEM_VARIANT",
Message: "Tanımsız ürün kombinasyonu",
ClientKey: safeNS(ln.ClientKey),
ItemCode: item,
ColorCode: color,
Dim1: dim1,
Dim2: dim2,
}
}
return nil
}
// ValidateOrderVariants: save/update öncesi payload satırlarını prItemVariant'a göre doğrular.
// invalid döner; error sadece DB/prepare/query hatalarında döner.
func ValidateOrderVariants(db *sql.DB, lines []models.OrderDetail) ([]models.InvalidVariant, error) {
normalizeEmpty := func(s string) string {
s = strings.TrimSpace(strings.ToUpper(s))
if s == "_" || s == "-" {
return ""
}
return s
}
stmt, err := db.Prepare(`
SELECT CASE WHEN EXISTS (
SELECT 1
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK)
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3
AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4
) THEN 1 ELSE 0 END
`)
if err != nil {
return nil, fmt.Errorf("validate prepare hatası: %w", err)
}
defer stmt.Close()
invalid := make([]models.InvalidVariant, 0)
for i, ln := range lines {
qty := qtyValue(ln.Qty1)
if qty <= 0 {
continue
}
item := normalizeEmpty(normalizeKeyPart(ln.ItemCode))
color := normalizeEmpty(normalizeKeyPart(ln.ColorCode))
dim1 := normalizeEmpty(normalizeKeyPart(ln.ItemDim1Code))
dim2 := normalizeEmpty(normalizeKeyPart(ln.ItemDim2Code))
// ItemCode boş ise invalid
if strings.TrimSpace(item) == "" {
invalid = append(invalid, models.InvalidVariant{
Index: i,
ClientKey: safeNS(ln.ClientKey),
ItemCode: item,
ColorCode: color,
Dim1: dim1,
Dim2: dim2,
Qty1: qty,
ComboKey: safeNS(ln.ComboKey),
Reason: "ItemCode boş",
})
continue
}
var exists int
if err := stmt.QueryRow(item, color, dim1, dim2).Scan(&exists); err != nil {
return nil, fmt.Errorf("validate query hatası (i=%d): %w", i, err)
}
if exists != 1 {
invalid = append(invalid, models.InvalidVariant{
Index: i,
ClientKey: safeNS(ln.ClientKey),
ItemCode: item,
ColorCode: color,
Dim1: dim1,
Dim2: dim2,
Qty1: qty,
ComboKey: safeNS(ln.ComboKey),
Reason: "prItemVariantta yok",
})
}
}
return invalid, nil
}
// =======================================================
// LineResult → frontend senkronu için
// =======================================================
type OrderLineResult struct {
ClientKey string `json:"clientKey"`
OrderLineID string `json:"orderLineID"`
}
// =======================================================
// PART 1 — InsertOrder (header + lines insert) — FINAL v5.1
// ✔ OrderHeaderID backend üretir
// ✔ LOCAL-... numara gelirse gerçek WS numarası üretir
// ✔ Full debug
// ✔ Tüm satırlar INSERT edilir
// ✔ INSERT öncesi ItemVariant Guard
// ✔ Payload içi Duplicate Guard (comboKey)
// =======================================================
func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) (string, []OrderLineResult, error) {
conn := db.GetDB()
fmt.Println("🟦 InsertOrder() BAŞLADI -----------------------------")
tx, err := conn.Begin()
if err != nil {
return "", nil, fmt.Errorf("tx baslatilamadi: %w", err)
}
defer tx.Rollback()
now := time.Now()
v3User := fmt.Sprintf("V3U%d-%s", user.V3UserGroup, user.V3Username)
// =======================================================
// 1) BACKEND — OrderHeaderID üretimi (HER ZAMAN)
// =======================================================
realHeaderID := uuid.New().String()
fmt.Println("🟩 Backend yeni OrderHeaderID üretti:", realHeaderID)
header.OrderHeaderID = realHeaderID
// =======================================================
// 2) OrderNumber üretimi (LOCAL-* ise gerçek WS numarası)
// =======================================================
if !header.OrderNumber.Valid ||
strings.HasPrefix(header.OrderNumber.String, "LOCAL-") ||
len(strings.TrimSpace(header.OrderNumber.String)) == 0 {
fmt.Println("🟨 LOCAL numara geldi → gerçek WS numarası üretilecek...")
var realNumber string
err := tx.QueryRow(`
SELECT CONCAT(
'1-WS-3-',
RIGHT('00000' + CAST(NEXT VALUE FOR BAGGI_V3.dbo.Seq_OrderNumber_WS AS VARCHAR(10)), 5)
)
`).Scan(&realNumber)
if err != nil {
return "", nil, fmt.Errorf("Gerçek sipariş numarası üretilemedi: %w", err)
}
fmt.Println("🟩 Üretilen gerçek WS numarası:", realNumber)
header.OrderNumber.String = realNumber
header.OrderNumber.Valid = true
}
newID := realHeaderID // artık DBye bu yazılacak
// =======================================================
// 3) Döviz kuru çözümü
// =======================================================
exRate := 1.0
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil {
if c.Rate > 0 {
exRate = c.Rate
}
}
}
// =======================================================
// 4) HEADER INSERT
// =======================================================
queryHeader := `
INSERT INTO BAGGI_V3.dbo.trOrderHeader (
OrderHeaderID, OrderTypeCode, ProcessCode, OrderNumber, IsCancelOrder,
OrderDate, OrderTime, DocumentNumber, PaymentTerm,
AverageDueDate, Description, InternalDescription,
CurrAccTypeCode, CurrAccCode, SubCurrAccID, ContactID,
ShipmentMethodCode, ShippingPostalAddressID, BillingPostalAddressID,
GuarantorContactID, GuarantorContactID2, RoundsmanCode,
DeliveryCompanyCode, TaxTypeCode, WithHoldingTaxTypeCode, DOVCode,
TaxExemptionCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode,
POSTerminalID, WarehouseCode, ToWarehouseCode,
OrdererCompanyCode, OrdererOfficeCode, OrdererStoreCode,
GLTypeCode, DocCurrencyCode, LocalCurrencyCode, ExchangeRate,
TDisRate1, TDisRate2, TDisRate3, TDisRate4, TDisRate5,
DiscountReasonCode, SurplusOrderQtyToleranceRate,
ImportFileNumber, ExportFileNumber,
IncotermCode1, IncotermCode2, LettersOfCreditNumber,
PaymentMethodCode, IsInclutedVat, IsCreditSale, IsCreditableConfirmed,
CreditableConfirmedUser, CreditableConfirmedDate,
IsSalesViaInternet, IsSuspended, IsCompleted, IsPrinted,
IsLocked, UserLocked, IsClosed,
ApplicationCode, ApplicationID,
CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate,
IsProposalBased
)
VALUES (
@p1,@p2,@p3,@p4,
@p5,@p6,@p7,@p8,
@p9,@p10,@p11,
@p12,@p13,@p14,@p15,
@p16,@p17,@p18,
@p19,@p20,@p21,
@p22,@p23,@p24,@p25,
@p26,@p27,@p28,@p29,@p30,
@p31,@p32,@p33,
@p34,@p35,@p36,
@p37,@p38,@p39,@p40,
@p41,@p42,@p43,@p44,@p45,
@p46,@p47,
@p48,@p49,
@p50,@p51,@p52,
@p53,@p54,@p55,@p56,@p57,
@p58,@p59,@p60,@p61,@p62,
@p63,@p64,@p65,
@p66,@p67,@p68,@p69,@p70,@p71,@p72,@p73
);
`
fmt.Println("🟪 HEADER INSERT ÇALIŞIYOR...")
headerParams := []any{
header.OrderHeaderID,
nullableInt16(header.OrderTypeCode, 1),
nullableString(header.ProcessCode, "WS"),
nullableString(header.OrderNumber, ""),
nullableBool(header.IsCancelOrder, false),
nullableDateString(header.OrderDate),
nullableTimeString(header.OrderTime),
nullableString(header.DocumentNumber, ""),
nullableInt16(header.PaymentTerm, 0),
nullableDateString(header.AverageDueDate),
nullableString(header.Description, ""),
nullableString(header.InternalDescription, ""),
nullableInt16(header.CurrAccTypeCode, 0),
nullableString(header.CurrAccCode, ""),
nullableUUID(header.SubCurrAccID),
nullableUUID(header.ContactID),
nullableString(header.ShipmentMethodCode, ""),
nullableUUID(header.ShippingPostalAddressID),
nullableUUID(header.BillingPostalAddressID),
nullableUUID(header.GuarantorContactID),
nullableUUID(header.GuarantorContactID2),
nullableString(header.RoundsmanCode, ""),
nullableString(header.DeliveryCompanyCode, ""),
nullableInt16(header.TaxTypeCode, 0),
nullableString(header.WithHoldingTaxTypeCode, ""),
nullableString(header.DOVCode, ""),
nullableInt16(header.TaxExemptionCode, 0),
nullableInt32ToInt16(header.CompanyCode, 1),
nullableString(header.OfficeCode, "101"),
nullableInt16(header.StoreTypeCode, 5),
nullableString(header.StoreCode, ""),
nullableInt16(header.POSTerminalID, 0),
nullableString(header.WarehouseCode, "1-0-12"),
nullableString(header.ToWarehouseCode, ""),
nullableInt32ToInt16(header.OrdererCompanyCode, 1),
nullableString(header.OrdererOfficeCode, "101"),
nullableString(header.OrdererStoreCode, ""),
nullableString(header.GLTypeCode, ""),
nullableString(header.DocCurrencyCode, "TRY"),
nullableString(header.LocalCurrencyCode, "TRY"),
nullableFloat64(header.ExchangeRate, exRate),
nullableFloat64(header.TDisRate1, 0),
nullableFloat64(header.TDisRate2, 0),
nullableFloat64(header.TDisRate3, 0),
nullableFloat64(header.TDisRate4, 0),
nullableFloat64(header.TDisRate5, 0),
nullableInt16(header.DiscountReasonCode, 0),
nullableFloat64(header.SurplusOrderQtyToleranceRate, 0),
nullableString(header.ImportFileNumber, ""),
nullableString(header.ExportFileNumber, ""),
nullableString(header.IncotermCode1, ""),
nullableString(header.IncotermCode2, ""),
nullableString(header.LettersOfCreditNumber, ""),
nullableString(header.PaymentMethodCode, ""),
nullableBool(header.IsInclutedVat, false),
nullableBool(header.IsCreditSale, true),
nullableBool(header.IsCreditableConfirmed, true),
nullableString(header.CreditableConfirmedUser, v3User),
nullableDateTime(header.CreditableConfirmedDate, now),
nullableBool(header.IsSalesViaInternet, false),
nullableBool(header.IsSuspended, false),
nullableBool(header.IsCompleted, false),
nullableBool(header.IsPrinted, false),
nullableBool(header.IsLocked, false),
nullableBool(header.UserLocked, false),
nullableBool(header.IsClosed, false),
nullableString(header.ApplicationCode, "Order"),
nullableUUID(header.ApplicationID),
nullableString(header.CreatedUserName, v3User),
nullableDateTimeString(header.CreatedDate, now),
nullableString(header.LastUpdatedUserName, v3User),
nullableDateTimeString(header.LastUpdatedDate, now),
nullableBool(header.IsProposalBased, false),
}
if _, err := tx.Exec(queryHeader, headerParams...); err != nil {
fmt.Println("❌ HEADER INSERT ERROR:", err)
return "", nil, fmt.Errorf("header insert hatasi: %w", err)
}
fmt.Println("🟩 HEADER INSERT OK — ID:", newID)
// =======================================================
// 5) LINE INSERT
// =======================================================
insStmt, err := tx.Prepare(`
INSERT INTO BAGGI_V3.dbo.trOrderLine (
OrderLineID,
SortOrder, ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, Qty2,
CancelQty1, CancelQty2,
OrderCancelReasonCode,
DeliveryDate, PlannedDateOfLading,
LineDescription,
UsedBarcode, CostCenterCode,
VatCode, VatRate, PCTCode, PCTRate,
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
Price,
BaseProcessCode, BaseOrderNumber,
BaseCustomerTypeCode, BaseCustomerCode,
BaseSubCurrAccID, BaseStoreCode,
OrderHeaderID,
CreatedUserName, CreatedDate,
LastUpdatedUserName, LastUpdatedDate,
SurplusOrderQtyToleranceRate,
WithHoldingTaxTypeCode,
DOVCode
)
VALUES (
@p1,@p2,@p3,@p4,
@p5,@p6,@p7,@p8,
@p9,@p10,
@p11,@p12,
@p13,
@p14,@p15,
@p16,
@p17,@p18,
@p19,@p20,@p21,@p22,
@p23,@p24,@p25,@p26,@p27,
@p28,@p29,@p30,
@p31,
@p32,@p33,
@p34,@p35,
@p36,@p37,
@p38,
@p39,@p40,
@p41,@p42,
@p43,
@p44,
@p45
)`)
if err != nil {
return "", nil, fmt.Errorf("line insert stmt hazirlanamadi: %w", err)
}
defer insStmt.Close()
lineResults := make([]OrderLineResult, 0, len(lines))
// ✅ Duplicate Guard (payload içi)
seenCombo := make(map[string]bool)
for i, ln := range lines {
// ===================== PART 2 (Satır 301-600) =====================
fmt.Println("────────────────────────────────────")
fmt.Printf("🟨 [INSERT] LINE %d — gelen OrderLineID=%s\n", i+1, ln.OrderLineID)
// Her satır için yeni GUID
if ln.OrderLineID == "" {
newLineID := uuid.New().String()
fmt.Println("🆕 Yeni LineID üretildi:", newLineID)
ln.OrderLineID = newLineID
}
// ✅ Duplicate Guard (comboKey)
comboKey := normalizeComboKey(safeNS(ln.ComboKey))
if comboKey == "" {
comboKey = makeComboKey(ln)
}
if qtyValue(ln.Qty1) > 0 && comboKey != "" {
if seenCombo[comboKey] {
return "", nil, fmt.Errorf(
"Duplicate satır (comboKey=%s, ClientKey=%s)",
comboKey,
safeNS(ln.ClientKey),
)
}
seenCombo[comboKey] = true
}
// V2 Logic → %0 PCT
vatCode := normalizeVatCode(ln.VatCode)
pctCode := normalizePCTCode(ln.PCTCode)
var pctParam any
if pctCode == "" {
pctParam = nil
} else {
pctParam = pctCode
}
planned := nullableDateString(ln.PlannedDateOfLading)
// ✅ INSERT ÖNCESİ ItemVariant GUARD
if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil {
fmt.Println("❌ VARIANT GUARD (INSERT):", err)
return "", nil, err
}
}
fmt.Printf(
"🚨 INSERT LINE[%d] | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
i+1,
ln.OrderLineID,
safeNS(ln.ClientKey),
safeNS(ln.ItemCode),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
safeNS(ln.ItemDim3Code),
nf0(ln.Qty1),
)
_, err := insStmt.Exec(
ln.OrderLineID,
ln.SortOrder,
ln.ItemTypeCode,
nullableString(ln.ItemCode, ""),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
nullableString(ln.ItemDim3Code, ""),
ln.Qty1, ln.Qty2,
ln.CancelQty1, ln.CancelQty2,
nullableString(ln.OrderCancelReasonCode, ""),
nullableTime(ln.DeliveryDate, now),
planned,
nullableString(ln.LineDescription, ""),
nullableString(ln.UsedBarcode, ""),
nullableString(ln.CostCenterCode, ""),
vatCode, nf0(ln.VatRate),
pctParam, nf0(ln.PCTRate),
nf0(ln.LDisRate1),
nf0(ln.LDisRate2),
nf0(ln.LDisRate3),
nf0(ln.LDisRate4),
nf0(ln.LDisRate5),
nullableString(ln.DocCurrencyCode, "TRY"),
nullableString(ln.PriceCurrencyCode, "TRY"),
nf0(ln.PriceExchangeRate),
nf0(ln.Price),
nullableString(ln.BaseProcessCode, ""),
nullableString(ln.BaseOrderNumber, ""),
ln.BaseCustomerTypeCode,
nullableString(ln.BaseCustomerCode, ""),
nullableUUID(ln.BaseSubCurrAccID),
nullableString(ln.BaseStoreCode, ""),
header.OrderHeaderID,
v3User, now,
v3User, now,
nf0(ln.SurplusOrderQtyToleranceRate),
nullableString(ln.WithHoldingTaxTypeCode, ""),
nullableString(ln.DOVCode, ""),
)
if err != nil {
fmt.Println("❌ INSERT LINE ERROR")
fmt.Printf(
"💥 FAILED LINE | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q\n",
ln.OrderLineID,
safeNS(ln.ClientKey),
safeNS(ln.ItemCode),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
safeNS(ln.ItemDim3Code),
)
fmt.Println("SQL ERROR:", err)
return "", nil, fmt.Errorf("line insert hatasi: %w", err)
}
if ln.ClientKey.Valid && ln.ClientKey.String != "" {
lineResults = append(lineResults, OrderLineResult{
ClientKey: ln.ClientKey.String,
OrderLineID: ln.OrderLineID,
})
}
}
// =======================================================
// 6) COMMIT
// =======================================================
fmt.Println("🟨 COMMIT EDİLİYOR...")
if err := tx.Commit(); err != nil {
fmt.Println("❌ COMMIT ERROR:", err)
return "", nil, err
}
fmt.Println("✅ COMMIT OK — INSERT ORDER BİTTİ")
fmt.Println("────────────────────────────────────")
return newID, lineResults, nil
}
// =======================================================
// PART 2 — UpdateOrder FULL DEBUG (v4.3)
// ✔ ComboKey ile açık satır eşleştirme
// ✔ Kapalı satırları korur
// ✔ Payload içi Duplicate Guard
// ✔ INSERT/UPDATE öncesi ItemVariant Guard (tek noktada)
// ✔ Gridde olmayan açık satırları siler (önce child)
// =======================================================
func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) ([]OrderLineResult, error) {
conn := db.GetDB()
// ======================================================
// 🔍 SCAN DEBUG — HEADER bilgisi
// ======================================================
fmt.Println("══════════════════════════════════════")
fmt.Println("🔍 [DEBUG] UpdateOrder çağrıldı")
fmt.Printf("🔍 HeaderID: %v\n", header.OrderHeaderID)
fmt.Printf("🔍 Line sayısı: %v\n", len(lines))
fmt.Printf("🔍 User: %v (V3: %s/%d)\n",
user.Username, user.V3Username, user.V3UserGroup)
fmt.Println("══════════════════════════════════════")
tx, err := conn.Begin()
if err != nil {
return nil, fmt.Errorf("tx baslatilamadi: %w", err)
}
defer tx.Rollback()
now := time.Now()
v3User := fmt.Sprintf("V3U%d-%s", user.V3UserGroup, user.V3Username)
// Döviz kuru
exRate := 1.0
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil && c.Rate > 0 {
exRate = c.Rate
}
}
// =======================================================
// 0) Mevcut satırları oku (GUID STRING olarak!)
// =======================================================
existingOpen := make(map[string]bool)
existingClosed := make(map[string]bool)
existingOpenCombo := make(map[string]string)
existingClosedCombo := make(map[string]string)
rows, err := tx.Query(`
SELECT
CONVERT(varchar(36), OrderLineID),
ISNULL(IsClosed, 0),
ISNULL(ItemCode,''),
ISNULL(ColorCode,''),
ISNULL(ItemDim1Code,''),
ISNULL(ItemDim2Code,'')
FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1
`, header.OrderHeaderID)
if err != nil {
return nil, fmt.Errorf("mevcut satirlar okunamadi: %w", err)
}
defer rows.Close()
for rows.Next() {
var id, item, color, dim1, dim2 string
var closed bool
if err := rows.Scan(&id, &closed, &item, &color, &dim1, &dim2); err != nil {
return nil, err
}
combo := makeComboKeyParts(item, color, dim1, dim2)
if closed {
existingClosed[id] = true
if combo != "" {
existingClosedCombo[combo] = id
}
} else {
existingOpen[id] = true
if combo != "" {
existingOpenCombo[combo] = id
}
}
}
// ======================================================
// HEADER UPDATE
// ======================================================
_, err = tx.Exec(`
UPDATE BAGGI_V3.dbo.trOrderHeader SET
OrderDate=@p1,
OrderTime=@p2,
AverageDueDate=@p3,
Description=@p4,
InternalDescription=@p5,
DocCurrencyCode=@p6,
LocalCurrencyCode=@p7,
ExchangeRate=@p8,
LastUpdatedUserName=@p9,
LastUpdatedDate=@p10
WHERE OrderHeaderID=@p11
`,
nullableDateString(header.OrderDate),
nullableTimeString(header.OrderTime),
nullableDateString(header.AverageDueDate),
nullableString(header.Description, ""),
nullableString(header.InternalDescription, ""),
nullableString(header.DocCurrencyCode, "TRY"),
nullableString(header.LocalCurrencyCode, "TRY"),
nullableFloat64(header.ExchangeRate, exRate),
v3User,
now,
header.OrderHeaderID,
)
if err != nil {
return nil, err
}
// ======================================================
// PREPARE STATEMENTS
// ======================================================
insStmt, err := tx.Prepare(`INSERT INTO BAGGI_V3.dbo.trOrderLine (
OrderLineID, SortOrder, ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, Qty2, CancelQty1, CancelQty2, OrderCancelReasonCode,
DeliveryDate, PlannedDateOfLading, LineDescription,
UsedBarcode, CostCenterCode,
VatCode, VatRate, PCTCode, PCTRate,
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
Price, BaseProcessCode, BaseOrderNumber,
BaseCustomerTypeCode, BaseCustomerCode,
BaseSubCurrAccID, BaseStoreCode,
OrderHeaderID, CreatedUserName, CreatedDate,
LastUpdatedUserName, LastUpdatedDate,
SurplusOrderQtyToleranceRate,
WithHoldingTaxTypeCode, DOVCode)
VALUES (
@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,
@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,
@p19,@p20,@p21,@p22,@p23,@p24,@p25,@p26,@p27,
@p28,@p29,@p30,@p31,@p32,@p33,@p34,@p35,
@p36,@p37,@p38,@p39,@p40,@p41,@p42,@p43,
@p44,@p45)`)
if err != nil {
return nil, err
}
defer insStmt.Close()
updStmt, err := tx.Prepare(`UPDATE BAGGI_V3.dbo.trOrderLine SET
SortOrder=@p1, ItemTypeCode=@p2, ItemCode=@p3, ColorCode=@p4,
ItemDim1Code=@p5, ItemDim2Code=@p6, ItemDim3Code=@p7,
Qty1=@p8, Qty2=@p9, CancelQty1=@p10, CancelQty2=@p11,
OrderCancelReasonCode=@p12,
DeliveryDate=@p13, PlannedDateOfLading=@p14,
LineDescription=@p15, UsedBarcode=@p16, CostCenterCode=@p17,
VatCode=@p18, VatRate=@p19, PCTCode=@p20, PCTRate=@p21,
LDisRate1=@p22, LDisRate2=@p23, LDisRate3=@p24,
LDisRate4=@p25, LDisRate5=@p26,
DocCurrencyCode=@p27, PriceCurrencyCode=@p28,
PriceExchangeRate=@p29, Price=@p30,
BaseProcessCode=@p31, BaseOrderNumber=@p32,
BaseCustomerTypeCode=@p33, BaseCustomerCode=@p34,
BaseSubCurrAccID=@p35, BaseStoreCode=@p36,
LastUpdatedUserName=@p37, LastUpdatedDate=@p38,
SurplusOrderQtyToleranceRate=@p39,
WithHoldingTaxTypeCode=@p40, DOVCode=@p41
WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
if err != nil {
return nil, err
}
defer updStmt.Close()
lineResults := make([]OrderLineResult, 0)
seenCombo := make(map[string]bool)
for _, ln := range lines {
comboKey := normalizeComboKey(safeNS(ln.ComboKey))
if comboKey == "" {
comboKey = makeComboKey(ln)
}
// Duplicate guard (SADECE aktif)
if qtyValue(ln.Qty1) > 0 && comboKey != "" {
if seenCombo[comboKey] {
return nil, fmt.Errorf("Duplicate satır: %s", comboKey)
}
seenCombo[comboKey] = true
}
// Kapalı satır
if ln.OrderLineID != "" && existingClosed[ln.OrderLineID] {
continue
}
if comboKey != "" {
if _, ok := existingClosedCombo[comboKey]; ok {
continue
}
}
// DELETE SIGNAL
if ln.OrderLineID != "" && qtyValue(ln.Qty1) <= 0 {
_, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, ln.OrderLineID)
if err != nil {
return nil, err
}
_, err = tx.Exec(`
DELETE FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
`, header.OrderHeaderID, ln.OrderLineID)
if err != nil {
return nil, err
}
delete(existingOpen, ln.OrderLineID)
delete(existingOpenCombo, comboKey)
continue
}
isNew := false
if ln.OrderLineID == "" {
if dbID, ok := existingOpenCombo[comboKey]; ok {
ln.OrderLineID = dbID
} else {
ln.OrderLineID = uuid.New().String()
isNew = true
}
}
if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil {
return nil, err
}
}
if isNew {
_, err := insStmt.Exec(
ln.OrderLineID, ln.SortOrder, ln.ItemTypeCode,
nullableString(ln.ItemCode, ""), safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code), safeNS(ln.ItemDim2Code),
nullableString(ln.ItemDim3Code, ""),
ln.Qty1, ln.Qty2, ln.CancelQty1, ln.CancelQty2,
nullableString(ln.OrderCancelReasonCode, ""),
nullableTime(ln.DeliveryDate, now),
nullableDateString(ln.PlannedDateOfLading),
nullableString(ln.LineDescription, ""),
nullableString(ln.UsedBarcode, ""),
nullableString(ln.CostCenterCode, ""),
normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
nf0(ln.LDisRate1), nf0(ln.LDisRate2),
nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
nullableString(ln.DocCurrencyCode, "TRY"),
nullableString(ln.PriceCurrencyCode, "TRY"),
nf0(ln.PriceExchangeRate), nf0(ln.Price),
nullableString(ln.BaseProcessCode, ""),
nullableString(ln.BaseOrderNumber, ""),
ln.BaseCustomerTypeCode,
nullableString(ln.BaseCustomerCode, ""),
nullableUUID(ln.BaseSubCurrAccID),
nullableString(ln.BaseStoreCode, ""),
header.OrderHeaderID,
v3User, now, v3User, now,
nf0(ln.SurplusOrderQtyToleranceRate),
nullableString(ln.WithHoldingTaxTypeCode, ""),
nullableString(ln.DOVCode, ""),
)
if err != nil {
return nil, err
}
} else {
_, err := updStmt.Exec(
ln.SortOrder, ln.ItemTypeCode,
nullableString(ln.ItemCode, ""),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
nullableString(ln.ItemDim3Code, ""),
nf0(ln.Qty1), nf0(ln.Qty2),
nf0(ln.CancelQty1), nf0(ln.CancelQty2),
nullableString(ln.OrderCancelReasonCode, ""),
nullableTime(ln.DeliveryDate, now),
nullableDateString(ln.PlannedDateOfLading),
nullableString(ln.LineDescription, ""),
nullableString(ln.UsedBarcode, ""),
nullableString(ln.CostCenterCode, ""),
normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
nf0(ln.LDisRate1), nf0(ln.LDisRate2),
nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
nullableString(ln.DocCurrencyCode, "TRY"),
nullableString(ln.PriceCurrencyCode, "TRY"),
nf0(ln.PriceExchangeRate), nf0(ln.Price),
nullableString(ln.BaseProcessCode, ""),
nullableString(ln.BaseOrderNumber, ""),
ln.BaseCustomerTypeCode,
nullableString(ln.BaseCustomerCode, ""),
nullableUUID(ln.BaseSubCurrAccID),
nullableString(ln.BaseStoreCode, ""),
v3User, now,
nf0(ln.SurplusOrderQtyToleranceRate),
nullableString(ln.WithHoldingTaxTypeCode, ""),
nullableString(ln.DOVCode, ""),
ln.OrderLineID,
)
if err != nil {
return nil, err
}
}
delete(existingOpen, ln.OrderLineID)
delete(existingOpenCombo, comboKey)
if ln.ClientKey.Valid {
lineResults = append(lineResults, OrderLineResult{
ClientKey: ln.ClientKey.String,
OrderLineID: ln.OrderLineID,
})
}
}
// Grid dışı kalan açık satırlar
for id := range existingOpen {
_, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, id)
if err != nil {
return nil, err
}
_, err = tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLine WHERE OrderLineID=@p1 AND ISNULL(IsClosed,0)=0`, id)
if err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return lineResults, nil
}

View File

@@ -0,0 +1,109 @@
package queries
// 🔹 Sipariş ekranı stok kontrolü (ürün + renk + 2. renk + beden)
// 🔧 Renk1 boşsa (aksesuar vb.) renksiz stokları da çeker
const GetOrderInventory = `
DECLARE @ProductCode NVARCHAR(30) = @p1;
DECLARE @ColorCode NVARCHAR(30) = @p2;
DECLARE @ColorCode2 NVARCHAR(30) = @p3;
-- 🔧 Normalize giriş parametreleri
SET @ColorCode = LTRIM(RTRIM(ISNULL(@ColorCode, '')));
SET @ColorCode2 = LTRIM(RTRIM(ISNULL(@ColorCode2, '')));
IF @ColorCode = ''
BEGIN
SET @ColorCode = '';
END
IF @ColorCode2 = ''
BEGIN
SET @ColorCode2 = NULL;
END
------------------------------------------------------------
-- 🔹 Ana sorgu
------------------------------------------------------------
SELECT
Inventory.ItemCode AS Urun_Kodu,
Inventory.ColorCode AS Renk_Kodu,
ISNULL((
SELECT TOP 1 ColorDescription
FROM cdColorDesc WITH(NOLOCK)
WHERE cdColorDesc.ColorCode = Inventory.ColorCode
AND cdColorDesc.LangCode = N'TR'
), '') AS Renk_Aciklamasi,
-- ✅ NULL bedenleri boş string olarak getir
ISNULL(Inventory.ItemDim1Code, '') AS Beden,
ISNULL(Inventory.ItemDim2Code, '') AS Yaka,
ROUND(
SUM(Inventory.InventoryQty1) -
(SUM(Inventory.ReserveQty1) + SUM(Inventory.DispOrderQty1) + SUM(Inventory.PickingQty1)),
cdUnitOfMeasure.RoundDigit
) AS Kullanilabilir_Envanter
FROM cdItem WITH (NOLOCK)
JOIN cdUnitOfMeasure WITH (NOLOCK)
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
JOIN (
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(CASE WHEN SourceTable = 'PickingStates' THEN Qty1 ELSE 0 END) AS PickingQty1,
SUM(CASE WHEN SourceTable = 'ReserveStates' THEN Qty1 ELSE 0 END) AS ReserveQty1,
SUM(CASE WHEN SourceTable = 'DispOrderStates' THEN Qty1 ELSE 0 END) AS DispOrderQty1,
SUM(CASE WHEN SourceTable = 'trStock' THEN (In_Qty1 - Out_Qty1) ELSE 0 END) AS InventoryQty1
FROM (
SELECT 'PickingStates' AS SourceTable, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, 0 AS In_Qty1, 0 AS Out_Qty1
FROM PickingStates
UNION ALL
SELECT 'ReserveStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, 0, 0
FROM ReserveStates
UNION ALL
SELECT 'DispOrderStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, 0, 0
FROM DispOrderStates
UNION ALL
SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
0, SUM(In_Qty1), SUM(Out_Qty1)
FROM trStock WITH (NOLOCK)
GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) AS SourceData
GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) AS Inventory
ON cdItem.ItemTypeCode = Inventory.ItemTypeCode
AND cdItem.ItemCode = Inventory.ItemCode
LEFT JOIN ProductAttributesFilter
ON ProductAttributesFilter.ItemCode = Inventory.ItemCode
WHERE
Inventory.ItemTypeCode IN (1)
AND Inventory.WarehouseCode IN ('1-0-12','1-0-21','1-0-10','1-0-2','1-1-3','1-2-4','1-2-5')
AND cdItem.IsBlocked = 0
AND Inventory.ItemCode = @ProductCode
AND (
-- 🔹 Eğer renk girilmişse o renk; boşsa renksiz stokları getir
(LTRIM(RTRIM(@ColorCode)) <> '' AND Inventory.ColorCode = @ColorCode)
OR (LTRIM(RTRIM(@ColorCode)) = '' AND (Inventory.ColorCode IS NULL OR LTRIM(RTRIM(Inventory.ColorCode)) = ''))
)
AND (
-- 🔹 2. renk sadece doluysa filtrelensin
@ColorCode2 IS NULL
OR Inventory.ItemDim2Code = @ColorCode2
)
GROUP BY
Inventory.ItemCode,
Inventory.ColorCode,
ISNULL(Inventory.ItemDim1Code, ''), -- ✅ NULL bedenleri boş string olarak gruplar
ISNULL(Inventory.ItemDim2Code, ''), -- ✅ NULL yakaları boş string olarak gruplar
cdUnitOfMeasure.RoundDigit;
`

213
svc/queries/orderlist.go Normal file
View File

@@ -0,0 +1,213 @@
package queries
import (
"bssapp-backend/auth"
"bssapp-backend/internal/authz"
"context"
"database/sql"
"fmt"
)
// ========================================================
// 📌 GetOrderList — FINAL + CURRENCY SAFE + PIYASA AUTHZ
// ========================================================
func GetOrderList(
ctx context.Context,
mssql *sql.DB,
pg *sql.DB,
search string,
) (*sql.Rows, error) {
claims, ok := auth.GetClaimsFromContext(ctx)
if !ok || claims == nil {
return nil, fmt.Errorf("unauthorized: claims not found")
}
// ----------------------------------------------------
// 🔐 PIYASA FILTER (ADMIN BYPASS)
// ----------------------------------------------------
piyasaWhere := "1=1"
if !claims.IsAdmin() {
codes, err := authz.GetUserPiyasaCodes(pg, int(claims.ID))
if err != nil {
return nil, fmt.Errorf("piyasa codes load error: %w", err)
}
if len(codes) == 0 {
// hiç yetkisi yok → hiç kayıt dönmesin
piyasaWhere = "1=0"
} else {
// ⚠️ EXISTS içinde kullanılacak
piyasaWhere = authz.BuildINClause(
"UPPER(f2.CustomerAtt01)",
codes,
)
}
}
// ----------------------------------------------------
// 📄 BASE QUERY
// ----------------------------------------------------
baseQuery := fmt.Sprintf(`
SELECT
CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
ISNULL(h.OrderNumber, '') AS OrderNumber,
CONVERT(varchar, h.OrderDate, 23) AS OrderDate,
ISNULL(h.CurrAccCode, '') AS CurrAccCode,
ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription,
ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi,
ISNULL(py.AttributeDescription, '') AS Piyasa,
CONVERT(varchar, h.CreditableConfirmedDate,23) AS CreditableConfirmedDate,
ISNULL(h.DocCurrencyCode,'TRY') AS DocCurrencyCode,
ISNULL(l.TotalAmount,0) AS TotalAmount,
----------------------------------------------------------------
-- USD HESABI (TRY / EUR / GBP / USD DESTEKLİ)
----------------------------------------------------------------
CASE
WHEN h.DocCurrencyCode = 'USD'
THEN ISNULL(l.TotalAmount,0)
WHEN h.DocCurrencyCode = 'TRY'
AND usd.Rate > 0
THEN ISNULL(l.TotalAmount,0) / usd.Rate
WHEN h.DocCurrencyCode IN ('EUR','GBP')
AND cur.Rate > 0
AND usd.Rate > 0
THEN (ISNULL(l.TotalAmount,0) * cur.Rate) / usd.Rate
ELSE 0
END AS TotalAmountUSD,
ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed,
ISNULL(h.Description,'') AS Description,
usd.Rate AS ExchangeRateUSD
FROM dbo.trOrderHeader h
JOIN (
SELECT
OrderHeaderID,
SUM(Qty1 * Price) AS TotalAmount
FROM dbo.trOrderLine
GROUP BY OrderHeaderID
) l
ON l.OrderHeaderID = h.OrderHeaderID
LEFT JOIN dbo.cdCurrAccDesc ca
ON ca.CurrAccCode = h.CurrAccCode
AND ca.LangCode = 'TR'
-- müşteri temsilcisi + piyasa açıklamaları
LEFT JOIN dbo.CustomerAttributesFilter f
ON f.CurrAccCode = h.CurrAccCode
LEFT JOIN dbo.cdCurrAccAttributeDesc mt
ON mt.CurrAccTypeCode = 3
AND mt.AttributeTypeCode = 2
AND mt.AttributeCode = f.CustomerAtt02
AND mt.LangCode = 'TR'
LEFT JOIN dbo.cdCurrAccAttributeDesc py
ON py.CurrAccTypeCode = 3
AND py.AttributeTypeCode = 1
AND py.AttributeCode = f.CustomerAtt01
AND py.LangCode = 'TR'
----------------------------------------------------------------
-- USD → TRY
----------------------------------------------------------------
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.AllExchangeRates
WHERE CurrencyCode = 'USD'
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(GETDATE() AS date)
ORDER BY Date DESC
) usd
----------------------------------------------------------------
-- ORDER PB → TRY
----------------------------------------------------------------
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.AllExchangeRates
WHERE CurrencyCode = h.DocCurrencyCode
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(GETDATE() AS date)
ORDER BY Date DESC
) cur
WHERE
ISNULL(h.IsCancelOrder,0) = 0
AND h.OrderTypeCode = 1
AND h.ProcessCode = 'WS'
AND h.IsClosed = 0
-- 🔐 PIYASA AUTHZ (EXISTS — SAĞLAM YOL)
AND EXISTS (
SELECT 1
FROM dbo.CustomerAttributesFilter f2
WHERE f2.CurrAccCode = h.CurrAccCode
AND %s
)
`, piyasaWhere)
// ----------------------------------------------------
// 🔍 SEARCH FILTER (CASE + TR SAFE)
// ----------------------------------------------------
if search != "" {
baseQuery += `
AND EXISTS (
SELECT 1
FROM dbo.trOrderHeader h2
LEFT JOIN dbo.cdCurrAccDesc ca2
ON ca2.CurrAccCode = h2.CurrAccCode
AND ca2.LangCode = 'TR'
WHERE h2.OrderHeaderID = h.OrderHeaderID
AND (
LOWER(REPLACE(REPLACE(h2.OrderNumber,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
OR LOWER(REPLACE(REPLACE(h2.CurrAccCode,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
OR LOWER(REPLACE(REPLACE(ca2.CurrAccDescription,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
OR LOWER(REPLACE(REPLACE(h2.Description,'İ','I'),'ı','i'))
COLLATE Latin1_General_CI_AI LIKE LOWER(@p1)
)
)
`
}
// ----------------------------------------------------
// 📌 ORDER
// ----------------------------------------------------
baseQuery += `
ORDER BY h.CreatedDate DESC
`
// ----------------------------------------------------
// ▶ EXECUTE
// ----------------------------------------------------
if search != "" {
searchLike := fmt.Sprintf("%%%s%%", search)
return mssql.Query(baseQuery, searchLike)
}
return mssql.Query(baseQuery)
}

View File

@@ -0,0 +1,75 @@
package queries
const OrderListBaseQuery = `
SELECT
CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
ISNULL(h.OrderNumber, '') AS OrderNumber,
CONVERT(varchar, h.OrderDate, 23) AS OrderDate,
ISNULL(h.CurrAccCode, '') AS CurrAccCode,
ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription,
ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi,
ISNULL(py.AttributeDescription, '') AS Piyasa,
CONVERT(varchar, h.CreditableConfirmedDate, 23) AS CreditableConfirmedDate,
ISNULL(h.DocCurrencyCode, 'TRY') AS DocCurrencyCode,
ISNULL(l.TotalAmount, 0) AS TotalAmount,
CASE
WHEN h.DocCurrencyCode = 'USD'
THEN ISNULL(l.TotalAmount, 0)
WHEN h.DocCurrencyCode = 'TRY'
THEN ISNULL(l.TotalAmount, 0) / NULLIF(er.Rate, 1)
ELSE 0
END AS TotalAmountUSD,
ISNULL(h.IsCreditableConfirmed, 0) AS IsCreditableConfirmed,
ISNULL(h.Description, '') AS Description,
ISNULL(er.Rate, 1) AS ExchangeRateUSD
FROM dbo.trOrderHeader h
JOIN (
SELECT OrderHeaderID, SUM(Qty1 * Price) AS TotalAmount
FROM dbo.trOrderLine
GROUP BY OrderHeaderID
) l ON l.OrderHeaderID = h.OrderHeaderID
LEFT JOIN dbo.cdCurrAccDesc ca
ON ca.CurrAccCode = h.CurrAccCode AND ca.LangCode = 'TR'
LEFT JOIN dbo.CustomerAttributes f
ON f.CurrAccTypeCode = h.CurrAccTypeCode
AND f.CurrAccCode = h.CurrAccCode
LEFT JOIN dbo.cdCurrAccAttributeDesc mt
ON mt.CurrAccTypeCode = f.CurrAccTypeCode
AND mt.AttributeTypeCode = 2
AND mt.AttributeCode = f.CustomerAtt02
AND mt.LangCode = 'TR'
LEFT JOIN dbo.cdCurrAccAttributeDesc py
ON py.CurrAccTypeCode = f.CurrAccTypeCode
AND py.AttributeTypeCode = 3
AND py.AttributeCode = f.CustomerAtt03
AND py.LangCode = 'TR'
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.AllExchangeRates er
WHERE er.CurrencyCode = 'USD'
AND er.RelationCurrencyCode = 'TRY'
AND er.ExchangeTypeCode = 6
AND er.Rate > 0
ORDER BY ABS(DATEDIFF(DAY, er.Date, GETDATE()))
) er
WHERE
ISNULL(h.IsCancelOrder, 0) = 0
AND h.OrderTypeCode = 1
AND h.ProcessCode = 'WS'
AND h.IsClosed = 0
`

View File

@@ -0,0 +1,36 @@
package queries
import (
"bssapp-backend/models"
"database/sql"
"fmt"
)
// GetOrderPriceListB2B → model + currency bazlı ürün fiyatını döndürür (PostgreSQL sürümü)
func GetOrderPriceListB2B(db *sql.DB, modelCode string, currency string) (*models.OrderPriceListB2B, error) {
query := `
SELECT
mmitem.code AS ModelCode,
sdprc.crn AS CurrencyCode,
sdprc.prc AS Price,
sdprc.sdprcgrp_id AS PriceGroupID,
TO_CHAR(sdprc.zlins_dttm, 'YYYY-MM-DD') AS LastUpdate
FROM sdprc
LEFT JOIN mmitem ON sdprc.mmitem_id = mmitem.id
WHERE mmitem.code = $1
AND sdprc.prc IS NOT NULL
AND sdprc.prc > 0
AND sdprc.crn = $2
AND sdprc.sdprcgrp_id = 1
ORDER BY sdprc.zlins_dttm DESC
LIMIT 1;
`
row := db.QueryRow(query, modelCode, currency)
var p models.OrderPriceListB2B
err := row.Scan(&p.ModelCode, &p.CurrencyCode, &p.Price, &p.PriceGroupID, &p.LastUpdate)
if err != nil {
return nil, fmt.Errorf("ürün fiyatı bulunamadı: %v", err)
}
return &p, nil
}

View File

@@ -0,0 +1,47 @@
package queries
/* ======================================================
ROLE + DEPARTMENT PERMISSIONS
====================================================== */
// GET
const GetRoleDepartmentPermissions = `
SELECT
rdp.module_code,
rdp.action,
rdp.allowed
FROM vw_role_dept_permissions rdp
WHERE rdp.role_id = $1
AND rdp.department_code = $2
ORDER BY rdp.module_code, rdp.action
`
// UPSERT
const UpsertRoleDepartmentPermission = `
INSERT INTO mk_sys_role_department_permissions
(
role_id,
department_code,
module_code,
action,
allowed
)
VALUES ($1,$2,$3,$4,$5)
ON CONFLICT ON CONSTRAINT uq_role_dept_module_action
DO UPDATE SET
allowed = EXCLUDED.allowed;
`
// ======================================================
// 📦 MODULES
// ======================================================
const GetModuleLookup = `
SELECT
code AS value,
name AS label
FROM mk_sys_modules
ORDER BY id
`

34
svc/queries/product.go Normal file
View File

@@ -0,0 +1,34 @@
package queries
import (
"bssapp-backend/db"
"bssapp-backend/models"
)
// GetProductList → MSSQL'den ürün listesini döndürür
func GetProductList() ([]models.Product, error) {
rows, err := db.MssqlDB.Query(`
SELECT
ProductCode
FROM ProductFilterWithDescription('TR')
WHERE
ProductAtt42 IN ('SERI', 'AKSESUAR')
AND IsBlocked = 0
AND LEN(ProductCode) = 13 -- 🔹 yalnızca 13 karakterlik kodlar
ORDER BY ProductCode;
`)
if err != nil {
return nil, err
}
defer rows.Close()
var list []models.Product
for rows.Next() {
var p models.Product
if err := rows.Scan(&p.ProductCode); err != nil {
return nil, err
}
list = append(list, p)
}
return list, nil
}

View File

@@ -0,0 +1,20 @@
package queries
const GetProductColors = `
DECLARE @ProductCode VARCHAR(30) = @p1;
SELECT DISTINCT
p.ProductCode,
v.ColorCode,
ISNULL(cd.ColorDescription, '') AS ColorDescription
FROM ProductFilterWithDescription('TR') AS p
INNER JOIN prItemVariant AS v WITH(NOLOCK)
ON v.ItemCode = p.ProductCode
LEFT JOIN cdColorDesc AS cd WITH(NOLOCK)
ON cd.ColorCode = v.ColorCode
AND cd.LangCode = 'TR'
WHERE
p.ProductCode = @ProductCode
AND ISNULL(v.ColorCode, '') <> ''
ORDER BY v.ColorCode;
`

View File

@@ -0,0 +1,66 @@
package queries
const GetProductColorSizes = `
------------------------------------------------------------
-- 🧩 GetProductColorSizes
-- Kullanım:
-- @p1 = ProductCode
-- @p2 = ColorCode
-- @p3 = ColorCode2
-- Açıklama:
-- Renk veya ikinci renk boşsa buna göre filtre uygular.
-- Renksiz (aksesuar) ürünlerde ColorCode='' kayıtlarını getirir.
------------------------------------------------------------
DECLARE @ProductCode NVARCHAR(30) = @p1;
DECLARE @ColorCode NVARCHAR(30) = @p2;
DECLARE @ColorCode2 NVARCHAR(30) = @p3;
-- 🔧 Normalize parametreler (boşlukları kırp ve düzelt)
SET @ColorCode = LTRIM(RTRIM(ISNULL(@ColorCode, '')));
SET @ColorCode2 = LTRIM(RTRIM(ISNULL(@ColorCode2, '')));
IF @ColorCode = ''
BEGIN
SET @ColorCode = ''; -- tamamen renksiz ürün
END
IF @ColorCode2 = ''
BEGIN
SET @ColorCode2 = NULL; -- 2. renk yok
END
------------------------------------------------------------
-- 🔹 Ana sorgu
------------------------------------------------------------
SELECT
Product.ProductCode,
ISNULL(prItemVariant.ColorCode, '') AS ColorCode,
ISNULL(prItemVariant.ItemDim1Code, '') AS ItemDim1Code,
ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code
FROM ProductFilterWithDescription('TR') AS Product
INNER JOIN cdItem WITH(NOLOCK)
ON cdItem.ItemTypeCode = 1
AND cdItem.ItemCode = Product.ProductCode
LEFT JOIN prProductLot WITH(NOLOCK)
ON prProductLot.ItemTypeCode = cdItem.ItemTypeCode
AND prProductLot.ItemCode = cdItem.ItemCode
AND prProductLot.IsDefault = 1
LEFT JOIN prItemVariant WITH(NOLOCK)
ON prItemVariant.ItemCode = Product.ProductCode
WHERE
ProductAtt42 IN ('SERI','AKSESUAR')
AND Product.IsBlocked = 0
AND Product.ProductCode = @ProductCode
AND (
-- 🔸 1. renk doluysa o renk
(LTRIM(RTRIM(@ColorCode)) <> '' AND prItemVariant.ColorCode = @ColorCode)
-- 🔸 1. renk boşsa renksiz varyantlar
OR (LTRIM(RTRIM(@ColorCode)) = '' AND (prItemVariant.ColorCode IS NULL OR LTRIM(RTRIM(prItemVariant.ColorCode)) = ''))
-- 🔸 2. renk varsa ek olarak dahil et
OR (@ColorCode2 IS NOT NULL AND prItemVariant.ColorCode = @ColorCode2)
)
ORDER BY
prItemVariant.ItemDim1Code,
prItemVariant.ItemDim2Code;
`

View File

@@ -0,0 +1,36 @@
package queries
const GetProductDetail = `
DECLARE @ProductCode VARCHAR(30) = @p1;
SELECT
ProductCode,
'' AS BOS7,
ProductDescription,
Product.ProductAtt42,
Product.ProductAtt42Desc AS URUN_ILK_GRUBU,
Product.ProductAtt01,
Product.ProductAtt01Desc AS URUN_ANA_GRUBU,
Product.ProductAtt02,
Product.ProductAtt02Desc AS URUN_ALT_GRUBU,
Product.ProductAtt41,
Product.ProductAtt41Desc AS ICERIK,
Product.ProductAtt10,
Product.ProductAtt10Desc AS MARKA,
Product.ProductAtt11,
Product.ProductAtt11Desc AS DROP_,
Product.ProductAtt29,
Product.ProductAtt29Desc AS KARISIM,
Product.ProductAtt45,
Product.ProductAtt45Desc AS ASKILI_YAN,
Product.ProductAtt44,
Product.ProductAtt44Desc AS KATEGORI,
Product.ProductAtt38,
Product.ProductAtt38Desc AS FIT1,
Product.ProductAtt39,
Product.ProductAtt39Desc AS FIT2
FROM ProductFilterWithDescription('TR') AS Product
WHERE ProductAtt42 IN ('SERI','AKSESUAR')
AND IsBlocked = 0
AND ProductCode = @p1;
`

View File

@@ -0,0 +1,18 @@
package queries
const GetProductSecondColors = `
SELECT DISTINCT
Product.ProductCode,
ISNULL(prItemVariant.ColorCode, '') AS ColorCode,
ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code
FROM prItemVariant WITH(NOLOCK)
INNER JOIN ProductFilterWithDescription('TR') AS Product
ON prItemVariant.ItemCode = Product.ProductCode
LEFT JOIN cdColorDesc AS ColorDesc WITH(NOLOCK)
ON ColorDesc.ColorCode = prItemVariant.ItemDim2Code
AND ColorDesc.LangCode = 'TR'
WHERE Product.ProductCode = @ProductCode
AND prItemVariant.ColorCode = @ColorCode
AND ISNULL(prItemVariant.ItemDim2Code, '') <> ''
GROUP BY Product.ProductCode, prItemVariant.ItemDim2Code, prItemVariant.ColorCode
`

View File

@@ -0,0 +1,172 @@
package queries
import (
"bssapp-backend/db"
"bssapp-backend/models"
"database/sql"
"fmt"
"strings"
)
// Ana tabloyu getiren fonksiyon (Vue header tablosu için)
func GetStatements(params models.StatementParams) ([]models.StatementHeader, error) {
// AccountCode normalize: "ZLA0127" → "ZLA 0127"
if len(params.AccountCode) == 7 && strings.ContainsAny(params.AccountCode, "0123456789") {
params.AccountCode = params.AccountCode[:3] + " " + params.AccountCode[3:]
}
// Parislemler []string → '1','2','3'
parislemFilter := "''"
if len(params.Parislemler) > 0 {
quoted := make([]string, len(params.Parislemler))
for i, v := range params.Parislemler {
quoted[i] = fmt.Sprintf("'%s'", v)
}
parislemFilter = strings.Join(quoted, ",")
}
query := fmt.Sprintf(`
;WITH Opening AS (
SELECT
b.CurrAccCode AS Cari_Kod,
b.DocCurrencyCode AS Para_Birimi,
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
FROM trCurrAccBook b
LEFT JOIN trCurrAccBookCurrency c
ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND b.DocumentDate < @startdate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
GROUP BY b.CurrAccCode, b.DocCurrencyCode
),
Movements AS (
SELECT
b.CurrAccCode AS Cari_Kod,
d.CurrAccDescription AS Cari_Isim,
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
b.RefNumber AS Belge_No,
b.BaseApplicationCode AS Islem_Tipi,
b.LineDescription AS Aciklama,
b.DocCurrencyCode AS Para_Birimi,
c.Debit AS Borc,
c.Credit AS Alacak,
SUM(c.Debit - c.Credit)
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
f.ATAtt01 AS Parislemtipi
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON b.CurrAccCode = d.CurrAccCode
LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode
LEFT JOIN CurrAccBookATAttributesFilter f
ON b.CurrAccBookID = f.CurrAccBookID
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND b.DocumentDate BETWEEN @startdate AND @enddate
AND EXISTS (
SELECT 1
FROM CurrAccBookATAttributesFilter f2
WHERE f2.CurrAccBookID = b.CurrAccBookID
AND f2.ATAtt01 IN (%s)
)
)
SELECT
m.Cari_Kod,
m.Cari_Isim,
m.Belge_Tarihi,
m.Vade_Tarihi,
m.Belge_No,
m.Islem_Tipi,
m.Aciklama,
m.Para_Birimi,
m.Borc,
m.Alacak,
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
m.Parislemtipi AS Parislemler
FROM Movements m
LEFT JOIN Opening o
ON o.Cari_Kod = m.Cari_Kod
AND o.Para_Birimi = m.Para_Birimi
UNION ALL
-- Devir satırı
SELECT
@Carikod AS Cari_Kod,
MAX(d.CurrAccDescription) AS Cari_Isim,
CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi,
CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
'Baslangic_devir' AS Belge_No,
'Devir' AS Islem_Tipi,
'Devir Bakiyesi' AS Aciklama,
b.DocCurrencyCode AS Para_Birimi,
SUM(c.Debit) AS Borc,
SUM(c.Credit) AS Alacak,
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
(
SELECT STRING_AGG(x.ATAtt01, ',')
FROM (
SELECT DISTINCT f2.ATAtt01
FROM CurrAccBookATAttributesFilter f2
INNER JOIN trCurrAccBook bb
ON f2.CurrAccBookID = bb.CurrAccBookID
WHERE bb.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND bb.DocumentDate < @startdate
AND f2.ATAtt01 IN (%s)
) x
) AS Parislemler
FROM trCurrAccBook b
LEFT JOIN cdCurrAccDesc d
ON b.CurrAccCode = d.CurrAccCode
LEFT JOIN trCurrAccBookCurrency c
ON b.CurrAccBookID = c.CurrAccBookID
AND b.DocCurrencyCode = c.CurrencyCode
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
AND b.DocumentDate < @startdate
GROUP BY b.DocCurrencyCode
ORDER BY Para_Birimi, Belge_Tarihi;
`, parislemFilter, parislemFilter, parislemFilter)
rows, err := db.MssqlDB.Query(query,
sql.Named("startdate", params.StartDate),
sql.Named("enddate", params.EndDate),
sql.Named("Carikod", params.AccountCode),
sql.Named("LangCode", params.LangCode),
)
if err != nil {
return nil, fmt.Errorf("MSSQL query error: %v", err)
}
defer rows.Close()
var results []models.StatementHeader
for rows.Next() {
var r models.StatementHeader
if err := rows.Scan(
&r.CariKod,
&r.CariIsim,
&r.BelgeTarihi,
&r.VadeTarihi,
&r.BelgeNo,
&r.IslemTipi,
&r.Aciklama,
&r.ParaBirimi,
&r.Borc,
&r.Alacak,
&r.Bakiye,
&r.Parislemler,
); err != nil {
return nil, err
}
results = append(results, r)
}
return results, nil
}

Some files were not shown because too many files have changed in this diff Show More