65 Commits

Author SHA1 Message Date
Daniel Goc
d73dad8975 merge and small bugfix 2026-04-16 15:02:38 +02:00
Daniel Goc
7995177fe1 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into expand_orders 2026-04-16 15:01:45 +02:00
70e0e23ace Merge pull request 'feat: implement logger' (#74) from logger into main
Reviewed-on: #74
Reviewed-by: goc_daniel <goc_daniel@ma-al.com>
2026-04-16 13:00:47 +00:00
9961d90fa7 feat: implement logger 2026-04-16 14:46:09 +02:00
Daniel Goc
f435a8839b expand orders 2026-04-16 11:47:55 +02:00
7bce04e05a Merge pull request 'is_oem' (#72) from is_oem into main
Reviewed-on: #72
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-15 11:32:43 +00:00
9a90de3f11 Merge pull request 'no-vat-customers' (#71) from no-vat-customers into main
Reviewed-on: #71
2026-04-15 11:32:13 +00:00
Daniel Goc
754bf2fe01 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into is_oem 2026-04-15 13:20:26 +02:00
Daniel Goc
2ca07f03ce expanded by is_oem 2026-04-15 13:19:28 +02:00
6efb39edf7 Merge branch 'no-vat-customers' of ssh://git.ma-al.com:8822/goc_daniel/b2b into no-vat-customers 2026-04-15 12:58:37 +02:00
e9af4bf311 feat: add no vat customers logic 2026-04-15 12:58:23 +02:00
cc570cc6a8 Merge branch 'main' into no-vat-customers 2026-04-15 10:57:16 +00:00
1bf706dcd0 feat: add no vat customers logic 2026-04-15 12:55:14 +02:00
84b4c70ffb Merge pull request 'bugfix' (#70) from countries_bugfix into main
Reviewed-on: #70
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-15 10:00:41 +00:00
Daniel Goc
2fd9472db1 bugfix 2026-04-15 11:48:29 +02:00
66df535317 Merge pull request 'expand_carts' (#69) from expand_carts into main
Reviewed-on: #69
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-15 09:19:16 +00:00
e31ecda582 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into no-vat-customers 2026-04-15 11:14:40 +02:00
Daniel Goc
e0a86febc4 add missing permission 2026-04-14 15:52:45 +02:00
Daniel Goc
40154ec861 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into expand_carts 2026-04-14 15:43:44 +02:00
Daniel Goc
bb507036db change add-product endpoint + remove-product 2026-04-14 15:42:30 +02:00
80a1314dc0 Merge pull request 'some small fixes' (#68) from small_fixes into main
Reviewed-on: #68
2026-04-14 13:16:17 +00:00
Daniel Goc
100a9f57d4 some small fixes 2026-04-14 14:08:57 +02:00
773e7d3c20 Merge pull request 'feat: lookup by id in customer search' (#61) from cust-search into main
Reviewed-on: #61
2026-04-14 11:42:56 +00:00
8e063978a8 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into no-vat-customers 2026-04-14 13:40:37 +02:00
03a0e5ea64 Merge branch 'main' into cust-search 2026-04-14 11:39:18 +00:00
ce8c19f715 Merge pull request 'feat: make routing per role, add unlogged role' (#67) from routing-per-role into main
Reviewed-on: #67
Reviewed-by: goc_daniel <goc_daniel@ma-al.com>
2026-04-14 11:39:13 +00:00
31a2744131 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into no-vat-customers 2026-04-14 13:36:11 +02:00
4edcb0a852 Merge branch 'main' into cust-search 2026-04-14 11:22:00 +00:00
a4120dafa2 Merge branch 'main' into routing-per-role 2026-04-14 11:21:53 +00:00
5e1a8e898c Merge pull request 'orders' (#58) from orders into main
Reviewed-on: #58
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-14 11:20:05 +00:00
Daniel Goc
c610ce38cc fixes after merging with main 2026-04-14 13:19:48 +02:00
8e3e41d6fe Merge branch 'main' into cust-search 2026-04-14 11:16:42 +00:00
b33da9d072 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into routing-per-role 2026-04-14 13:15:51 +02:00
Daniel Goc
604247b7c8 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into orders 2026-04-14 13:14:52 +02:00
f55d59a0fd feat: add no vat property to customers 2026-04-14 13:12:21 +02:00
e5988a85f3 Merge pull request 'update_categories' (#62) from update_categories into main
Reviewed-on: #62
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-14 10:35:28 +00:00
Daniel Goc
0cb5cc47bb Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into update_categories 2026-04-14 12:33:11 +02:00
Daniel Goc
1efc5417be permissions strings change 2026-04-14 12:32:24 +02:00
Daniel Goc
a0c3dd8ec8 added filtering on is_new and is_favorite 2026-04-14 12:28:39 +02:00
ab783b599d chore: add favorite field to base product query 2026-04-14 11:07:55 +02:00
d173af29fe fix: actually add the unlogged role to migration 2026-04-14 10:18:12 +02:00
f14d60d67b chore: swap permission string in handler to consts 2026-04-14 10:17:05 +02:00
967b101f9b feat: make routing per role, add unlogged role 2026-04-14 09:54:37 +02:00
97ca510b99 Merge branch 'main' into cust-search 2026-04-14 06:26:47 +00:00
26cbdeec0a Merge pull request 'product-procedures' (#59) from product-procedures into main
Reviewed-on: #59
Reviewed-by: goc_daniel <goc_daniel@ma-al.com>
2026-04-13 13:32:28 +00:00
Daniel Goc
ce4cadaa16 most importantly: new category and filter on is_new 2026-04-13 15:29:21 +02:00
Daniel Goc
7f05d39b38 Merge branch 'product-procedures' of ssh://git.ma-al.com:8822/goc_daniel/b2b into update_categories 2026-04-13 14:48:28 +02:00
83b7cd49dd feat: lookup by id in customer search 2026-04-13 14:43:18 +02:00
Daniel Goc
88255776f3 fixes 2026-04-13 14:29:36 +02:00
38cb07f3d4 chore: address pull request review issues 2026-04-13 14:22:14 +02:00
Daniel Goc
1f6d5ecb72 go routine and removing cart 2026-04-13 09:21:33 +02:00
2e61cde742 Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into product-procedures 2026-04-10 15:26:36 +02:00
Daniel Goc
d4d55e2757 send email when creating new order 2026-04-10 15:17:29 +02:00
54608410ea feat: create specific price system and adapt product queries 2026-04-10 15:04:34 +02:00
Daniel Goc
80d26bba12 GET -> POST 2026-04-10 14:57:24 +02:00
Daniel Goc
33e9d016e9 basic orders are ready 2026-04-10 14:53:40 +02:00
Daniel Goc
a03a2b461f small fix 2026-04-10 13:25:00 +02:00
Daniel Goc
134bc4ea53 code ready, time for testing... 2026-04-10 13:23:51 +02:00
Daniel Goc
8595969c6e Find is done 2026-04-10 12:17:52 +02:00
Daniel Goc
a6aa06faa0 basic setup 2026-04-10 11:17:58 +02:00
Daniel Goc
4f4b32b131 order struct 2026-04-10 11:06:43 +02:00
Daniel Goc
dfdf8b4db9 orders handler init 2026-04-10 11:01:39 +02:00
Daniel Goc
438a13c04c orders tables 2026-04-10 10:34:44 +02:00
8024d9f739 Merge pull request 'favorites' (#57) from favorites into main
Reviewed-on: #57
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-10 08:09:13 +00:00
75af44b0df feat: product_attribute list with prices 2026-04-09 09:51:06 +02:00
109 changed files with 3874 additions and 1141 deletions

5
.env
View File

@@ -63,4 +63,7 @@ FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
CORS_ORGIN=https://www.naluconcept.com CORS_ORGIN=https://www.naluconcept.com
DSN=root:Maal12345678@tcp(localhost:3306)/nalu DSN=root:Maal12345678@tcp(localhost:3306)/nalu
LOG_LEVEL=warn
LOG_COLORIZE=true

View File

@@ -4,8 +4,11 @@ import (
"log" "log"
"os" "os"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web" "git.ma-al.com/goc_daniel/b2b/app/delivery/web"
"git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/service/langsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/version" "git.ma-al.com/goc_daniel/b2b/app/utils/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -21,6 +24,8 @@ var (
return return
} }
logger.Init("b2b", nil, config.Get().Log.LogLevel, config.Get().Log.LogColorize)
// Create and setup the server // Create and setup the server
server := web.New() server := web.New()

View File

@@ -28,6 +28,7 @@ type Config struct {
Cors CorsConfig Cors CorsConfig
MeiliSearch MeiliSearchConfig MeiliSearch MeiliSearchConfig
Storage StorageConfig Storage StorageConfig
Log LogConfig
} }
type I18n struct { type I18n struct {
@@ -87,6 +88,11 @@ type AppConfig struct {
BaseURL string `env:"APP_BASE_URL,http://localhost:5173"` BaseURL string `env:"APP_BASE_URL,http://localhost:5173"`
} }
type LogConfig struct {
LogLevel string `env:"LOG_LEVEL,warn"`
LogColorize bool `env:"LOG_COLORIZE,true"`
}
type EmailConfig struct { type EmailConfig struct {
SMTPHost string `env:"EMAIL_SMTP_HOST,localhost"` SMTPHost string `env:"EMAIL_SMTP_HOST,localhost"`
SMTPPort int `env:"EMAIL_SMTP_PORT,587"` SMTPPort int `env:"EMAIL_SMTP_PORT,587"`
@@ -209,6 +215,11 @@ func load() *Config {
if err != nil { if err != nil {
slog.Error("not possible to load env variables for storage : ", err.Error(), "") slog.Error("not possible to load env variables for storage : ", err.Error(), "")
} }
err = loadEnv(&cfg.Log)
if err != nil {
slog.Error("not possible to load env variables for logger : ", err.Error(), "")
}
cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder) cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder)
return cfg return cfg

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/authService" "git.ma-al.com/goc_daniel/b2b/app/service/authService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
@@ -16,9 +17,8 @@ import (
) )
// AuthMiddleware creates authentication middleware // AuthMiddleware creates authentication middleware
func AuthMiddleware() fiber.Handler { func Authenticate() fiber.Handler {
authService := authService.NewAuthService() authService := authService.NewAuthService()
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
// Get token from Authorization header // Get token from Authorization header
authHeader := c.Get("Authorization") authHeader := c.Get("Authorization")
@@ -26,17 +26,13 @@ func AuthMiddleware() fiber.Handler {
// Try to get from cookie // Try to get from cookie
authHeader = c.Cookies("access_token") authHeader = c.Cookies("access_token")
if authHeader == "" { if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "authorization token required",
})
} }
} else { } else {
// Extract token from "Bearer <token>" // Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ") parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" { if len(parts) != 2 || parts[0] != "Bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "invalid authorization header format",
})
} }
authHeader = parts[1] authHeader = parts[1]
} }
@@ -44,24 +40,18 @@ func AuthMiddleware() fiber.Handler {
// Validate token // Validate token
claims, err := authService.ValidateToken(authHeader) claims, err := authService.ValidateToken(authHeader)
if err != nil { if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "invalid or expired token",
})
} }
// Get user from database // Get user from database
user, err := authService.GetUserByID(claims.UserID) user, err := authService.GetUserByID(claims.UserID)
if err != nil { if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Next()
"error": "user not found",
})
} }
// Check if user is active // Check if user is active
if !user.IsActive { if !user.IsActive {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ return c.Next()
"error": "user account is inactive",
})
} }
// Create locale. LangID is overwritten by auth Token // Create locale. LangID is overwritten by auth Token
@@ -79,10 +69,8 @@ func AuthMiddleware() fiber.Handler {
} }
// We now populate the target user // We now populate the target user
if model.CustomerRole(user.Role.Name) != model.RoleAdmin { if !userLocale.OriginalUser.HasPermission(perms.Teleport) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ return c.Next()
"error": "admin access required",
})
} }
targetUserID, err := strconv.Atoi(targetUserIDAttribute) targetUserID, err := strconv.Atoi(targetUserIDAttribute)
@@ -115,22 +103,14 @@ func AuthMiddleware() fiber.Handler {
} }
} }
// RequireAdmin creates admin-only middleware func Authorize() fiber.Handler {
func RequireAdmin() fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
originalUserRole, ok := localeExtractor.GetOriginalUserRole(c) _, ok := localeExtractor.GetUserID(c)
if !ok { if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "not authenticated", "error": "not authenticated",
}) })
} }
if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required",
})
}
return c.Next() return c.Next()
} }
} }

View File

@@ -2,27 +2,27 @@ package middleware
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
func Require(p perms.Permission) fiber.Handler { func Require(p perms.Permission) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
u := c.Locals("user") user, ok := localeExtractor.GetCustomer(c)
if u == nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
user, ok := u.(*model.UserSession)
if !ok { if !ok {
return c.SendStatus(fiber.StatusInternalServerError) return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
for _, perm := range user.Permissions { for _, perm := range user.Role.Permissions {
if perm == p { if perm.Name == p {
return c.Next() return c.Next()
} }
} }
return c.SendStatus(fiber.StatusForbidden) return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrForbidden)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrForbidden)))
} }
} }

View File

@@ -3,8 +3,16 @@ package perms
type Permission string type Permission string
const ( const (
UserReadAny Permission = "user.read.any" UserReadAny Permission = "user.read.any"
UserWriteAny Permission = "user.write.any" UserWriteAny Permission = "user.write.any"
UserDeleteAny Permission = "user.delete.any" UserDeleteAny Permission = "user.delete.any"
CurrencyWrite Permission = "currency.write" CurrencyWrite Permission = "currency.write"
SpecificPriceManage Permission = "specific_price.manage"
WebdavCreateToken Permission = "webdav.create_token"
ProductTranslationSave Permission = "product_translation.save"
ProductTranslationTranslate Permission = "product_translation.translate"
SearchCreateIndex Permission = "search.create_index"
OrdersViewAll Permission = "orders.view_all"
OrdersModifyAll Permission = "orders.modify_all"
Teleport Permission = "teleport"
) )

View File

@@ -1,7 +1,6 @@
package public package public
import ( import (
"log"
"strconv" "strconv"
"time" "time"
@@ -11,6 +10,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/authService" "git.ma-al.com/goc_daniel/b2b/app/service/authService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -49,7 +49,7 @@ func AuthHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/google", handler.GoogleLogin) r.Get("/google", handler.GoogleLogin)
r.Get("/google/callback", handler.GoogleCallback) r.Get("/google/callback", handler.GoogleCallback)
authProtected := r.Group("", middleware.AuthMiddleware()) authProtected := r.Group("", middleware.Authorize())
authProtected.Get("/me", handler.Me) authProtected.Get("/me", handler.Me)
authProtected.Post("/update-choice", handler.UpdateJWTToken) authProtected.Post("/update-choice", handler.UpdateJWTToken)
@@ -178,7 +178,13 @@ func (h *AuthHandler) ForgotPassword(c fiber.Ctx) error {
// Request password reset - always return success to prevent email enumeration // Request password reset - always return success to prevent email enumeration
err := h.authService.RequestPasswordReset(req.Email) err := h.authService.RequestPasswordReset(req.Email)
if err != nil { if err != nil {
log.Printf("Password reset request error: %v", err)
logger.Warn("password reset request failed",
"handler", "AuthHandler.ForgotPassword",
"email", req.Email,
"error", err.Error(),
)
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
@@ -307,7 +313,6 @@ func (h *AuthHandler) Register(c fiber.Ctx) error {
// Attempt registration // Attempt registration
err := h.authService.Register(&req) err := h.authService.Register(&req)
if err != nil { if err != nil {
log.Printf("Register error: %v", err)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
@@ -447,7 +452,6 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code) response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code)
if err != nil { if err != nil {
log.Printf("Google OAuth callback error: %v", err)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })

View File

@@ -2,6 +2,7 @@ package public
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -31,12 +32,21 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router {
} }
func (h *RoutingHandler) GetRouting(c fiber.Ctx) error { func (h *RoutingHandler) GetRouting(c fiber.Ctx) error {
lang_id, ok := localeExtractor.GetLangID(c) langId, ok := localeExtractor.GetLangID(c)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
menu, err := h.menuService.GetRoutes(lang_id)
var roleId uint
customer, ok := localeExtractor.GetCustomer(c)
if !ok {
roleId = constdata.UNLOGGED_USER_ROLE_ID
} else {
roleId = customer.RoleID
}
menu, err := h.menuService.GetRoutes(langId, roleId)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -6,6 +6,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService" "git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -45,6 +46,13 @@ func (h *AddressesHandler) GetTemplate(c fiber.Ctx) error {
template, err := h.addressesService.GetTemplate(uint(country_id)) template, err := h.addressesService.GetTemplate(uint(country_id))
if err != nil { if err != nil {
logger.Error("failed to get address template",
"handler", "AddressesHandler.GetTemplate",
"country_id", country_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -74,6 +82,13 @@ func (h *AddressesHandler) AddNewAddress(c fiber.Ctx) error {
err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id)) err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id))
if err != nil { if err != nil {
logger.Error("failed to add new address",
"handler", "AddressesHandler.AddNewAddress",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -110,6 +125,14 @@ func (h *AddressesHandler) ModifyAddress(c fiber.Ctx) error {
err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id)) err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id))
if err != nil { if err != nil {
logger.Error("failed to modify address",
"handler", "AddressesHandler.ModifyAddress",
"user_id", userID,
"address_id", address_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -124,13 +147,20 @@ func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID) addresses, err := h.addressesService.RetrieveAddresses(userID)
if err != nil { if err != nil {
logger.Error("failed to retrieve addresses",
"handler", "AddressesHandler.RetrieveAddressesInfo",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(addresses, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error { func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {
@@ -149,6 +179,14 @@ func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {
err = h.addressesService.DeleteAddress(userID, uint(address_id)) err = h.addressesService.DeleteAddress(userID, uint(address_id))
if err != nil { if err != nil {
logger.Error("failed to delete address",
"handler", "AddressesHandler.DeleteAddress",
"user_id", userID,
"address_id", address_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -6,6 +6,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/cartsService" "git.ma-al.com/goc_daniel/b2b/app/service/cartsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -29,10 +30,12 @@ func CartsHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCartsHandler() handler := NewCartsHandler()
r.Get("/add-new-cart", handler.AddNewCart) r.Get("/add-new-cart", handler.AddNewCart)
r.Delete("/remove-cart", handler.RemoveCart)
r.Get("/change-cart-name", handler.ChangeCartName) r.Get("/change-cart-name", handler.ChangeCartName)
r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo) r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo)
r.Get("/retrieve-cart", handler.RetrieveCart) r.Get("/retrieve-cart", handler.RetrieveCart)
r.Get("/add-product-to-cart", handler.AddProduct) r.Get("/add-product-to-cart", handler.AddProduct)
r.Delete("/remove-product-from-cart", handler.RemoveProduct)
return r return r
} }
@@ -44,8 +47,16 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
new_cart, err := h.cartsService.CreateNewCart(userID) name := c.Query("name")
new_cart, err := h.cartsService.CreateNewCart(userID, name)
if err != nil { if err != nil {
logger.Error("failed to create cart",
"handler", "CartsHandler.AddNewCart",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -53,6 +64,37 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
return c.JSON(response.Make(&new_cart, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&new_cart, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *CartsHandler) RemoveCart(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.cartsService.RemoveCart(userID, uint(cart_id))
if err != nil {
logger.Error("failed to remove cart",
"handler", "CartsHandler.RemoveCart",
"user_id", userID,
"cart_id", cart_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error { func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := localeExtractor.GetUserID(c)
if !ok { if !ok {
@@ -71,6 +113,14 @@ func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
err = h.cartsService.UpdateCartName(userID, uint(cart_id), new_name) err = h.cartsService.UpdateCartName(userID, uint(cart_id), new_name)
if err != nil { if err != nil {
logger.Error("failed to update cart name",
"handler", "CartsHandler.ChangeCartName",
"user_id", userID,
"cart_id", cart_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -87,6 +137,13 @@ func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
carts_info, err := h.cartsService.RetrieveCartsInfo(userID) carts_info, err := h.cartsService.RetrieveCartsInfo(userID)
if err != nil { if err != nil {
logger.Error("failed to retrieve carts info",
"handler", "CartsHandler.RetrieveCartsInfo",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -110,6 +167,14 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
cart, err := h.cartsService.RetrieveCart(userID, uint(cart_id)) cart, err := h.cartsService.RetrieveCart(userID, uint(cart_id))
if err != nil { if err != nil {
logger.Error("failed to retrieve cart",
"handler", "CartsHandler.RetrieveCart",
"user_id", userID,
"cart_id", cart_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -117,6 +182,7 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
return c.JSON(response.Make(cart, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(cart, 0, i18n.T_(c, response.Message_OK)))
} }
// adds or sets given amount of products to the cart
func (h *CartsHandler) AddProduct(c fiber.Ctx) error { func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c) userID, ok := localeExtractor.GetUserID(c)
if !ok { if !ok {
@@ -159,8 +225,78 @@ func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, uint(amount)) set_amount_attribute := c.Query("set_amount")
set_amount, err := strconv.ParseBool(set_amount_attribute)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, amount, set_amount)
if err != nil {
logger.Error("failed to add product to cart",
"handler", "CartsHandler.AddProduct",
"user_id", userID,
"cart_id", cart_id,
"product_id", product_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// removes product from the cart.
func (h *CartsHandler) RemoveProduct(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
product_id_attribute := c.Query("product_id")
product_id, err := strconv.Atoi(product_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
product_attribute_id_attribute := c.Query("product_attribute_id")
var product_attribute_id *uint
if product_attribute_id_attribute == "" {
product_attribute_id = nil
} else {
val, err := strconv.Atoi(product_attribute_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
uval := uint(val)
product_attribute_id = &uval
}
err = h.cartsService.RemoveProduct(userID, uint(cart_id), uint(product_id), product_attribute_id)
if err != nil {
logger.Error("failed to remove product from cart",
"handler", "CartsHandler.RemoveProduct",
"user_id", userID,
"cart_id", cart_id,
"product_id", product_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,6 +9,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/currencyService" "git.ma-al.com/goc_daniel/b2b/app/service/currencyService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -46,6 +47,12 @@ func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error {
err := h.CurrencyService.CreateCurrencyRate(&currencyRate) err := h.CurrencyService.CreateCurrencyRate(&currencyRate)
if err != nil { if err != nil {
logger.Error("failed to create currency rate",
"handler", "CurrencyHandler.PostCurrencyRate",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -63,6 +70,13 @@ func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
currency, err := h.CurrencyService.GetCurrency(uint(id)) currency, err := h.CurrencyService.GetCurrency(uint(id))
if err != nil { if err != nil {
logger.Error("failed to get currency",
"handler", "CurrencyHandler.GetCurrencyRate",
"currency_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -3,11 +3,13 @@ package restricted
import ( import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/customerService" "git.ma-al.com/goc_daniel/b2b/app/service/customerService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
@@ -30,7 +32,8 @@ func CustomerHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCustomerHandler() handler := NewCustomerHandler()
r.Get("", handler.customerData) r.Get("", handler.customerData)
r.Get("/list", handler.listCustomers) r.Get("/list", middleware.Require(perms.UserReadAny), handler.listCustomers)
r.Patch("/no-vat", middleware.Require(perms.UserWriteAny), handler.setCustomerNoVatStatus)
return r return r
} }
@@ -62,6 +65,13 @@ func (h *customerHandler) customerData(fc fiber.Ctx) error {
customer, err := h.service.GetById(customerId) customer, err := h.service.GetById(customerId)
if err != nil { if err != nil {
logger.Error("failed to get customer",
"handler", "customerHandler.customerData",
"customer_id", customerId,
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(err)). return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
} }
@@ -75,10 +85,6 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error {
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
} }
if !user.HasPermission(perms.UserReadAny) {
return fc.Status(fiber.StatusForbidden).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
}
p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers) p, filt, err := query_params.ParseFilters[model.Customer](fc, columnMappingListUsers)
if err != nil { if err != nil {
@@ -87,15 +93,15 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error {
} }
search := fc.Query("search") search := fc.Query("search")
if search != "" {
if !user.HasPermission(perms.UserReadAny) {
return fc.Status(fiber.StatusForbidden).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
}
}
customer, err := h.service.Find(user.LangID, p, filt, search) customer, err := h.service.Find(user.LangID, p, filt, search)
if err != nil { if err != nil {
logger.Error("failed to list customers",
"handler", "customerHandler.listCustomers",
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(err)). return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
} }
@@ -109,3 +115,35 @@ var columnMappingListUsers map[string]string = map[string]string{
"first_name": "users.first_name", "first_name": "users.first_name",
"last_name": "users.last_name", "last_name": "users.last_name",
} }
func (h *customerHandler) setCustomerNoVatStatus(fc fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(fc)
if !ok || user == nil {
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrInvalidBody)))
}
var req struct {
CustomerID uint `json:"customer_id"`
IsNoVat bool `json:"is_no_vat"`
}
if err := fc.Bind().Body(&req); err != nil {
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrJSONBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrJSONBody)))
}
if err := h.service.SetCustomerNoVatStatus(req.CustomerID, req.IsNoVat); err != nil {
logger.Error("failed to set customer no vat status",
"handler", "customerHandler.setCustomerNoVatStatus",
"customer_id", req.CustomerID,
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
}
return fc.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(fc, response.Message_OK)))
}

View File

@@ -3,6 +3,7 @@ package restricted
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService" "git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -34,6 +35,12 @@ func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error { func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
languages, err := h.localeSelectorService.GetLanguages() languages, err := h.localeSelectorService.GetLanguages()
if err != nil { if err != nil {
logger.Error("failed to get languages",
"handler", "LocaleSelectorHandler.GetLanguages",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -44,6 +51,12 @@ func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error { func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error {
countries, err := h.localeSelectorService.GetCountriesAndCurrencies() countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
if err != nil { if err != nil {
logger.Error("failed to get countries",
"handler", "LocaleSelectorHandler.GetCountries",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -6,6 +6,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -49,6 +50,12 @@ func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
category_tree, err := h.menuService.GetCategoryTree(uint(root_category_id), lang_id) category_tree, err := h.menuService.GetCategoryTree(uint(root_category_id), lang_id)
if err != nil { if err != nil {
logger.Error("failed to get category tree",
"handler", "MenuHandler.GetCategoryTree",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -79,6 +86,12 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id) breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id)
if err != nil { if err != nil {
logger.Error("failed to get breadcrumb",
"handler", "MenuHandler.GetBreadcrumb",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -94,6 +107,12 @@ func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
} }
menu, err := h.menuService.GetTopMenu(customer.LangID, customer.RoleID) menu, err := h.menuService.GetTopMenu(customer.LangID, customer.RoleID)
if err != nil { if err != nil {
logger.Error("failed to get top menu",
"handler", "MenuHandler.GetTopMenu",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -0,0 +1,204 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/orderService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type OrdersHandler struct {
ordersService *orderService.OrderService
}
func NewOrdersHandler() *OrdersHandler {
ordersService := orderService.New()
return &OrdersHandler{
ordersService: ordersService,
}
}
func OrdersHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewOrdersHandler()
r.Get("/list", handler.ListOrders)
r.Post("/place-new-order", handler.PlaceNewOrder)
r.Post("/change-order-address", handler.ChangeOrderAddress)
r.Get("/change-order-status", handler.ChangeOrderStatus)
return r
}
// when a user (not admin) wants to list orders, we automatically append filter to only view his orders.
// we base permissions and user based on target user only.
func (h *OrdersHandler) ListOrders(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
paging, filters, err := query_params.ParseFilters[model.CustomerOrder](c, columnMappingListOrders)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
list, err := h.ordersService.Find(user, paging, filters)
if err != nil {
logger.Error("failed to list orders",
"handler", "OrdersHandler.ListOrders",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
}
var columnMappingListOrders map[string]string = map[string]string{
"order_id": "b2b_customer_orders.order_id",
"user_id": "b2b_customer_orders.user_id",
"name": "b2b_customer_orders.name",
"country_id": "b2b_customer_orders.country_id",
"status": "b2b_customer_orders.status",
"base_price": "b2b_customer_orders.base_price",
"tax_incl": "b2b_customer_orders.tax_incl",
"tax_excl": "b2b_customer_orders.tax_excl",
"created_at": "b2b_customer_orders.created_at",
"updated_at": "b2b_customer_orders.updated_at",
}
func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
name := c.Query("name")
err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info)
if err != nil {
logger.Error("failed to place order",
"handler", "OrdersHandler.PlaceNewOrder",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// we base permissions and user based on target user only.
func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info)
if err != nil {
logger.Error("failed to change order address",
"handler", "OrdersHandler.ChangeOrderAddress",
"order_id", order_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// we base permissions and user based on target user only.
// TODO: well, permissions and all that.
func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
status := c.Query("status")
err = h.ordersService.ChangeOrderStatus(user, uint(order_id), status)
if err != nil {
logger.Error("failed to change order status",
"handler", "OrdersHandler.ChangeOrderStatus",
"order_id", order_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -4,10 +4,12 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
"git.ma-al.com/goc_daniel/b2b/app/service/productService" "git.ma-al.com/goc_daniel/b2b/app/service/productService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
@@ -34,6 +36,7 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/:id/:country_id/:quantity", handler.GetProductJson) r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
r.Get("/list", handler.ListProducts) r.Get("/list", handler.ListProducts)
r.Get("/list-variants/:product_id", handler.ListProductVariants)
r.Post("/favorite/:product_id", handler.AddToFavorites) r.Post("/favorite/:product_id", handler.AddToFavorites)
r.Delete("/favorite/:product_id", handler.RemoveFromFavorites) r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
@@ -70,8 +73,17 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
productJson, err := h.productService.GetJSON(p_id_product, int(customer.LangID), int(customer.ID), b2b_id_country, p_quantity) productJson, err := h.productService.Get(uint(p_id_product), customer.LangID, customer.ID, uint(b2b_id_country), uint(p_quantity))
if err != nil { if err != nil {
logger.Error("failed to get product",
"handler", "ProductsHandler.GetProductJson",
"product_id", p_id_product,
"lang_id", customer.LangID,
"customer_id", customer.ID,
"b2b_id_country", b2b_id_country,
"quantity", p_quantity,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -80,26 +92,27 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
} }
func (h *ProductsHandler) ListProducts(c fiber.Ctx) error { func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts) paging, filters, err := query_params.ParseFilters[dbmodel.PsProduct](c, columnMappingListProducts)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
id_lang, ok := localeExtractor.GetLangID(c) customer, ok := localeExtractor.GetCustomer(c)
if !ok { if !ok || customer == nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
userID, ok := localeExtractor.GetUserID(c) list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, constdata.DEFAULT_PRODUCT_QUANTITY, constdata.SHOP_ID)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
list, err := h.productService.Find(id_lang, userID, paging, filters)
if err != nil { if err != nil {
logger.Error("failed to list products",
"handler", "ProductsHandler.ListProducts",
"lang_id", customer.LangID,
"customer_id", customer.ID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -107,14 +120,16 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
} }
// These are all the filterable fields
var columnMappingListProducts map[string]string = map[string]string{ var columnMappingListProducts map[string]string = map[string]string{
"product_id": "ps.id_product", "product_id": "bp.product_id",
"name": "pl.name", "name": "bp.name",
"reference": "p.reference", "reference": "bp.reference",
"category_name": "cl.name", "category_id": "bp.category_id",
"category_id": "cp.id_category", "quantity": "bp.quantity",
"quantity": "sa.quantity", "is_favorite": "bp.is_favorite",
"is_favorite": "ps.is_favorite", "is_new": "bp.is_new",
"is_oem": "bp.is_oem",
} }
func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error { func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
@@ -134,6 +149,13 @@ func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
err = h.productService.AddToFavorites(userID, uint(productID)) err = h.productService.AddToFavorites(userID, uint(productID))
if err != nil { if err != nil {
logger.Error("failed to add to favorites",
"handler", "ProductsHandler.AddToFavorites",
"user_id", userID,
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -158,9 +180,48 @@ func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error {
err = h.productService.RemoveFromFavorites(userID, uint(productID)) err = h.productService.RemoveFromFavorites(userID, uint(productID))
if err != nil { if err != nil {
logger.Error("failed to remove from favorites",
"handler", "ProductsHandler.RemoveFromFavorites",
"user_id", userID,
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
} }
func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
customer, ok := localeExtractor.GetCustomer(c)
if !ok || customer == nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, constdata.DEFAULT_PRODUCT_QUANTITY)
if err != nil {
logger.Error("failed to list product variants",
"handler", "ProductsHandler.ListProductVariants",
"product_id", productID,
"customer_id", customer.ID,
"lang_id", customer.LangID,
"country_id", customer.CountryID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&list, len(list), i18n.T_(c, response.Message_OK)))
}

View File

@@ -4,10 +4,12 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService" "git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -35,8 +37,8 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewProductTranslationHandler() handler := NewProductTranslationHandler()
r.Get("/get-product-description", handler.GetProductDescription) r.Get("/get-product-description", handler.GetProductDescription)
r.Post("/save-product-description", handler.SaveProductDescription) r.Post("/save-product-description", middleware.Require(perms.ProductTranslationSave), handler.SaveProductDescription)
r.Get("/translate-product-description", handler.TranslateProductDescription) r.Get("/translate-product-description", middleware.Require(perms.ProductTranslationTranslate), handler.TranslateProductDescription)
return r return r
} }
@@ -65,6 +67,13 @@ func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
description, err := h.productTranslationService.GetProductDescription(userID, uint(productID), uint(productLangID)) description, err := h.productTranslationService.GetProductDescription(userID, uint(productID), uint(productLangID))
if err != nil { if err != nil {
logger.Error("failed to get product description",
"handler", "ProductTranslationHandler.GetProductDescription",
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -80,12 +89,6 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { if err != nil {
@@ -108,6 +111,13 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
err = h.productTranslationService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates) err = h.productTranslationService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates)
if err != nil { if err != nil {
logger.Error("failed to save product description",
"handler", "ProductTranslationHandler.SaveProductDescription",
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -123,12 +133,6 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID") productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute) productID, err := strconv.Atoi(productID_attribute)
if err != nil { if err != nil {
@@ -158,6 +162,13 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err
description, err := h.productTranslationService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel) description, err := h.productTranslationService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel)
if err != nil { if err != nil {
logger.Error("failed to translate product description",
"handler", "ProductTranslationHandler.TranslateProductDescription",
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -2,13 +2,14 @@ package restricted
import ( import (
"encoding/json" "encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService" "git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService" searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -30,7 +31,7 @@ func NewMeiliSearchHandler() *MeiliSearchHandler {
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router { func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMeiliSearchHandler() handler := NewMeiliSearchHandler()
r.Get("/create-index", handler.CreateIndex) r.Get("/create-index", middleware.Require(perms.SearchCreateIndex), handler.CreateIndex)
r.Post("/search", handler.Search) r.Post("/search", handler.Search)
r.Post("/settings", handler.GetSettings) r.Post("/settings", handler.GetSettings)
@@ -44,15 +45,15 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
err := h.meiliService.CreateIndex(id_lang) err := h.meiliService.CreateIndex(id_lang)
if err != nil { if err != nil {
fmt.Printf("CreateIndex error: %v\n", err)
logger.Error("failed to create search index",
"handler", "MeiliSearchHandler.CreateIndex",
"lang_id", id_lang,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -77,6 +78,13 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
result, err := h.searchService.Search(index, c.Body(), id_lang) result, err := h.searchService.Search(index, c.Body(), id_lang)
if err != nil { if err != nil {
logger.Error("failed to search",
"handler", "MeiliSearchHandler.Search",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -85,6 +93,13 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
if createErr := h.meiliService.CreateIndex(id_lang); createErr == nil { if createErr := h.meiliService.CreateIndex(id_lang); createErr == nil {
result, err = h.searchService.Search(index, c.Body(), id_lang) result, err = h.searchService.Search(index, c.Body(), id_lang)
if err != nil { if err != nil {
logger.Error("failed to search after index creation",
"handler", "MeiliSearchHandler.Search",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -105,6 +120,13 @@ func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error {
result, err := h.searchService.GetIndexSettings(index) result, err := h.searchService.GetIndexSettings(index)
if err != nil { if err != nil {
logger.Error("failed to get index settings",
"handler", "MeiliSearchHandler.GetSettings",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -0,0 +1,208 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/specificPriceService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type SpecificPriceHandler struct {
SpecificPriceService *specificPriceService.SpecificPriceService
config *config.Config
}
func NewSpecificPriceHandler() *SpecificPriceHandler {
SpecificPriceService := specificPriceService.New()
return &SpecificPriceHandler{
SpecificPriceService: SpecificPriceService,
config: config.Get(),
}
}
func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewSpecificPriceHandler()
r.Post("/", middleware.Require(perms.SpecificPriceManage), handler.Create)
r.Put("/:id", middleware.Require(perms.SpecificPriceManage), handler.Update)
r.Delete("/:id", middleware.Require(perms.SpecificPriceManage), handler.Delete)
r.Get("/", middleware.Require(perms.SpecificPriceManage), handler.List)
r.Get("/:id", middleware.Require(perms.SpecificPriceManage), handler.GetByID)
r.Patch("/:id/activate", middleware.Require(perms.SpecificPriceManage), handler.Activate)
r.Patch("/:id/deactivate", middleware.Require(perms.SpecificPriceManage), handler.Deactivate)
return r
}
func (h *SpecificPriceHandler) Create(c fiber.Ctx) error {
var pr model.SpecificPrice
if err := c.Bind().Body(&pr); err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
result, err := h.SpecificPriceService.Create(c.Context(), &pr)
if err != nil {
logger.Error("failed to create specific price",
"handler", "SpecificPriceHandler.Create",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Update(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
var pr model.SpecificPrice
if err := c.Bind().Body(&pr); err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
result, err := h.SpecificPriceService.Update(c.Context(), id, &pr)
if err != nil {
logger.Error("failed to update specific price",
"handler", "SpecificPriceHandler.Update",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) List(c fiber.Ctx) error {
result, err := h.SpecificPriceService.List(c.Context())
if err != nil {
logger.Error("failed to list specific prices",
"handler", "SpecificPriceHandler.List",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) GetByID(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
result, err := h.SpecificPriceService.GetByID(c.Context(), id)
if err != nil {
logger.Error("failed to get specific price",
"handler", "SpecificPriceHandler.GetByID",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Activate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.SetActive(c.Context(), id, true)
if err != nil {
logger.Error("failed to activate specific price",
"handler", "SpecificPriceHandler.Activate",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Deactivate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.SetActive(c.Context(), id, false)
if err != nil {
logger.Error("failed to deactivate specific price",
"handler", "SpecificPriceHandler.Deactivate",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Delete(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.Delete(c.Context(), id)
if err != nil {
logger.Error("failed to delete specific price",
"handler", "SpecificPriceHandler.Delete",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -4,10 +4,12 @@ import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/storageService" "git.ma-al.com/goc_daniel/b2b/app/service/storageService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -34,7 +36,7 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/download-file/*", handler.DownloadFile) r.Get("/download-file/*", handler.DownloadFile)
// for admins only // for admins only
r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken) r.Get("/create-new-webdav-token", middleware.Require(perms.WebdavCreateToken), handler.CreateNewWebdavToken)
return r return r
} }
@@ -51,6 +53,12 @@ func (h *StorageHandler) ListContent(c fiber.Ctx) error {
entries_in_list, err := h.storageService.ListContent(abs_path) entries_in_list, err := h.storageService.ListContent(abs_path)
if err != nil { if err != nil {
logger.Error("failed to list storage content",
"handler", "StorageHandler.ListContent",
"path", abs_path,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -67,6 +75,12 @@ func (h *StorageHandler) DownloadFile(c fiber.Ctx) error {
f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path) f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path)
if err != nil { if err != nil {
logger.Error("failed to prepare file download",
"handler", "StorageHandler.DownloadFile",
"path", abs_path,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -84,14 +98,14 @@ func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
new_token, err := h.storageService.NewWebdavToken(userID) new_token, err := h.storageService.NewWebdavToken(userID)
if err != nil { if err != nil {
logger.Error("failed to create webdav token",
"handler", "StorageHandler.CreateNewWebdavToken",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -86,9 +86,10 @@ func (s *Server) Setup() error {
// API routes // API routes
s.api = s.app.Group("/api/v1") s.api = s.app.Group("/api/v1")
s.api.Use(middleware.Authenticate())
s.public = s.api.Group("/public") s.public = s.api.Group("/public")
s.restricted = s.api.Group("/restricted") s.restricted = s.api.Group("/restricted")
s.restricted.Use(middleware.AuthMiddleware()) s.restricted.Use(middleware.Authorize())
s.webdav = s.api.Group("/webdav") s.webdav = s.api.Group("/webdav")
s.webdav.Use(middleware.Webdav()) s.webdav.Use(middleware.Webdav())
@@ -132,6 +133,13 @@ func (s *Server) Setup() error {
carts := s.restricted.Group("/carts") carts := s.restricted.Group("/carts")
restricted.CartsHandlerRoutes(carts) restricted.CartsHandlerRoutes(carts)
// orders (restricted)
orders := s.restricted.Group("/orders")
restricted.OrdersHandlerRoutes(orders)
specificPrice := s.restricted.Group("/specific-price")
restricted.SpecificPriceHandlerRoutes(specificPrice)
// addresses (restricted) // addresses (restricted)
addresses := s.restricted.Group("/addresses") addresses := s.restricted.Group("/addresses")
restricted.AddressesHandlerRoutes(addresses) restricted.AddressesHandlerRoutes(addresses)
@@ -159,16 +167,6 @@ func (s *Server) Setup() error {
// }) // })
// }) // })
// // Admin routes example
// admin := s.api.Group("/admin")
// admin.Use(middleware.AuthMiddleware())
// admin.Use(middleware.RequireAdmin())
// admin.Get("/users", func(c fiber.Ctx) error {
// return c.JSON(fiber.Map{
// "message": "Admin area - user management",
// })
// })
// keep this at the end because its wilderange // keep this at the end because its wilderange
general.InitBo(s.App()) general.InitBo(s.App())

View File

@@ -1,25 +1,18 @@
package model package model
type Address struct { type Address struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo string `gorm:"column:address_info;not null" json:"address_info"` AddressString string `gorm:"column:address_string;not null" json:"address_string"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
} }
func (Address) TableName() string { func (Address) TableName() string {
return "b2b_addresses" return "b2b_addresses"
} }
type AddressUnparsed struct { type AddressUnparsed interface{}
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
}
type AddressField interface {
}
// Address template in Poland // Address template in Poland
type AddressPL struct { type AddressPL struct {

View File

@@ -10,7 +10,8 @@ type ScannedCategory struct {
LinkRewrite string `gorm:"column:link_rewrite"` LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"` IsoCode string `gorm:"column:iso_code"`
Visited bool //this is for internal backend use only Visited bool // this is for internal backend use only
Filter string // filter applicable to this category
} }
type Category struct { type Category struct {
@@ -25,6 +26,7 @@ type CategoryParams struct {
CategoryID uint `json:"category_id" form:"category_id"` CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"` LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"` Locale string `json:"locale" form:"locale"`
Filter string `json:"filter" form:"filter"`
} }
type CategoryInBreadcrumb struct { type CategoryInBreadcrumb struct {

View File

@@ -1,15 +1,13 @@
package model package model
import "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
// Represents a country together with its associated currency // Represents a country together with its associated currency
type Country struct { type Country struct {
ID uint `gorm:"primaryKey;column:id" json:"id"` ID uint `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"` Name string `gorm:"column:name" json:"name"`
Flag string `gorm:"size:16;not null;column:flag" json:"flag"` Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
PSCurrencyID uint `gorm:"column:currency_id" json:"currency_id"` CurrencyID uint `gorm:"column:b2b_id_currency" json:"currency_id"`
PSCurrency *dbmodel.PsCurrency `gorm:"foreignKey:PSCurrencyID;references:IDCurrency" json:"ps_currency"` Currency *Currency `gorm:"foreignKey:CurrencyID" json:"currency,omitempty"`
} }
func (Country) TableName() string { func (Country) TableName() string {

View File

@@ -31,9 +31,11 @@ type Customer struct {
LastLoginAt *time.Time `json:"last_login_at,omitempty"` LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country
Country *Country `gorm:"foreignKey:CountryID" json:"country,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
IsNoVat bool `gorm:"default:false" json:"is_no_vat"`
} }
func (u *Customer) HasPermission(permission perms.Permission) bool { func (u *Customer) HasPermission(permission perms.Permission) bool {

34
app/model/order.go Normal file
View File

@@ -0,0 +1,34 @@
package model
import "time"
type CustomerOrder struct {
OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"`
UserID uint `gorm:"column:user_id;not null;index" json:"user_id"`
Name string `gorm:"column:name;not null" json:"name"`
CountryID uint `gorm:"column:country_id;not null" json:"country_id"`
AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
Status string `gorm:"column:status;size:50;not null" json:"status"`
BasePrice float64 `gorm:"column:base_price;type:decimal(10,2);not null" json:"base_price"`
TaxIncl float64 `gorm:"column:tax_incl;type:decimal(10,2);not null" json:"tax_incl"`
TaxExcl float64 `gorm:"column:tax_excl;type:decimal(10,2);not null" json:"tax_excl"`
CreatedAt time.Time `gorm:"column:created_at;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null" json:"updated_at"`
Products []OrderProduct `gorm:"foreignKey:OrderID;references:OrderID" json:"products"`
}
func (CustomerOrder) TableName() string {
return "b2b_customer_orders"
}
type OrderProduct struct {
OrderID uint `gorm:"column:order_id;not null;index" json:"-"`
ProductID uint `gorm:"column:product_id;not null" json:"product_id"`
ProductAttributeID *uint `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"`
Amount uint `gorm:"column:amount;not null" json:"amount"`
}
func (OrderProduct) TableName() string {
return "b2b_orders_products"
}

View File

@@ -1,76 +1,19 @@
package model package model
// Product contains each and every column from the table ps_product.
type Product struct {
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
SupplierID uint `gorm:"column:id_supplier" json:"supplier_id" form:"supplier_id"`
ManufacturerID uint `gorm:"column:id_manufacturer" json:"manufacturer_id" form:"manufacturer_id"`
CategoryDefaultID uint `gorm:"column:id_category_default" json:"category_default_id" form:"category_default_id"`
ShopDefaultID uint `gorm:"column:id_shop_default" json:"shop_default_id" form:"shop_default_id"`
TaxRulesGroupID uint `gorm:"column:id_tax_rules_group" json:"tax_rules_group_id" form:"tax_rules_group_id"`
OnSale uint `gorm:"column:on_sale" json:"on_sale" form:"on_sale"`
OnlineOnly uint `gorm:"column:online_only" json:"online_only" form:"online_only"`
EAN13 string `gorm:"column:ean13;type:varchar(13)" json:"ean13" form:"ean13"`
ISBN string `gorm:"column:isbn;type:varchar(32)" json:"isbn" form:"isbn"`
UPC string `gorm:"column:upc;type:varchar(12)" json:"upc" form:"upc"`
EkoTax float32 `gorm:"column:eko_tax;type:decimal(20,6)" json:"eko_tax" form:"eko_tax"`
Quantity uint `gorm:"column:quantity" json:"quantity" form:"quantity"`
MinimalQuantity uint `gorm:"column:minimal_quantity" json:"minimal_quantity" form:"minimal_quantity"`
LowStockThreshold uint `gorm:"column:low_stock_threshold" json:"low_stock_threshold" form:"low_stock_threshold"`
LowStockAlert uint `gorm:"column:low_stock_alert" json:"low_stock_alert" form:"low_stock_alert"`
Price float32 `gorm:"column:price;type:decimal(20,6)" json:"price" form:"price"`
WholesalePrice float32 `gorm:"column:wholesale_price;type:decimal(20,6)" json:"wholesale_price" form:"wholesale_price"`
Unity string `gorm:"column:unity;type:varchar(255)" json:"unity" form:"unity"`
UnitPriceRatio float32 `gorm:"column:unit_price_ratio;type:decimal(20,6)" json:"unit_price_ratio" form:"unit_price_ratio"`
UnitID uint `gorm:"column:id_unit;primaryKey" json:"unit_id" form:"unit_id"`
AdditionalShippingCost float32 `gorm:"column:additional_shipping_cost;type:decimal(20,2)" json:"additional_shipping_cost" form:"additional_shipping_cost"`
Reference string `gorm:"column:reference;type:varchar(64)" json:"reference" form:"reference"`
SupplierReference string `gorm:"column:supplier_reference;type:varchar(64)" json:"supplier_reference" form:"supplier_reference"`
Location string `gorm:"column:location;type:varchar(64)" json:"location" form:"location"`
Width float32 `gorm:"column:width;type:decimal(20,6)" json:"width" form:"width"`
Height float32 `gorm:"column:height;type:decimal(20,6)" json:"height" form:"height"`
Depth float32 `gorm:"column:depth;type:decimal(20,6)" json:"depth" form:"depth"`
Weight float32 `gorm:"column:weight;type:decimal(20,6)" json:"weight" form:"weight"`
OutOfStock uint `gorm:"column:out_of_stock" json:"out_of_stock" form:"out_of_stock"`
AdditionalDeliveryTimes uint `gorm:"column:additional_delivery_times" json:"additional_delivery_times" form:"additional_delivery_times"`
QuantityDiscount uint `gorm:"column:quantity_discount" json:"quantity_discount" form:"quantity_discount"`
Customizable uint `gorm:"column:customizable" json:"customizable" form:"customizable"`
UploadableFiles uint `gorm:"column:uploadable_files" json:"uploadable_files" form:"uploadable_files"`
TextFields uint `gorm:"column:text_fields" json:"text_fields" form:"text_fields"`
Active uint `gorm:"column:active" json:"active" form:"active"`
RedirectType string `gorm:"column:redirect_type;type:enum('','404','301-product','302-product','301-category','302-category')" json:"redirect_type" form:"redirect_type"`
TypeRedirectedID int `gorm:"column:id_type_redirected" json:"type_redirected_id" form:"type_redirected_id"`
AvailableForOrder uint `gorm:"column:available_for_order" json:"available_for_order" form:"available_for_order"`
AvailableDate string `gorm:"column:available_date;type:date" json:"available_date" form:"available_date"`
ShowCondition uint `gorm:"column:show_condition" json:"show_condition" form:"show_condition"`
Condition string `gorm:"column:condition;type:enum('new','used','refurbished')" json:"condition" form:"condition"`
ShowPrice uint `gorm:"column:show_price" json:"show_price" form:"show_price"`
Indexed uint `gorm:"column:indexed" json:"indexed" form:"indexed"`
Visibility string `gorm:"column:visibility;type:enum('both','catalog','search','none')" json:"visibility" form:"visibility"`
CacheIsPack uint `gorm:"column:cache_is_pack" json:"cache_is_pack" form:"cache_is_pack"`
CacheHasAttachments uint `gorm:"column:cache_has_attachments" json:"cache_has_attachments" form:"cache_has_attachments"`
IsVirtual uint `gorm:"column:is_virtual" json:"is_virtual" form:"is_virtual"`
CacheDefaultAttribute uint `gorm:"column:cache_default_attribute" json:"cache_default_attribute" form:"cache_default_attribute"`
DateAdd string `gorm:"column:date_add;type:datetime" json:"date_add" form:"date_add"`
DateUpd string `gorm:"column:date_upd;type:datetime" json:"date_upd" form:"date_upd"`
AdvancedStockManagement uint `gorm:"column:advanced_stock_management" json:"advanced_stock_management" form:"advanced_stock_management"`
PackStockType uint `gorm:"column:pack_stock_type" json:"pack_stock_type" form:"pack_stock_type"`
State uint `gorm:"column:state" json:"state" form:"state"`
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
}
type ProductInList struct { type ProductInList struct {
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"` ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
Name string `gorm:"column:name" json:"name" form:"name"` Name string `gorm:"column:name" json:"name" form:"name"`
LinkRewrite string `gorm:"column:link_rewrite" json:"link_rewrite"` LinkRewrite string `gorm:"column:link_rewrite" json:"link_rewrite"`
ImageLink string `gorm:"column:image_link" json:"image_link"` ImageLink string `gorm:"column:image_link" json:"image_link"`
CategoryName string `gorm:"column:category_name" json:"category_name" form:"category_name"` CategoryName string `gorm:"column:category_name" json:"category_name" form:"category_name"`
Reference string `gorm:"column:reference" json:"reference"` Reference string `gorm:"column:reference" json:"reference"`
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"` VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
Quantity int64 `gorm:"column:quantity" json:"quantity"` Quantity int64 `gorm:"column:quantity" json:"quantity"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"` PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
IsNew bool `gorm:"column:is_new" json:"is_new"`
IsOEM bool `gorm:"column:is_oem" json:"is_oem"`
} }
type ProductFilters struct { type ProductFilters struct {

View File

@@ -7,7 +7,6 @@ type Route struct {
Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"` Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"`
Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"` Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"`
Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"` Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"`
SortOrder *int `gorm:"type:int;default:0" json:"sort_order,omitempty"`
} }
func (Route) TableName() string { func (Route) TableName() string {

View File

@@ -0,0 +1,29 @@
package model
import "time"
type SpecificPrice struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
ValidFrom *time.Time `gorm:"null" json:"valid_from"`
ValidTill *time.Time `gorm:"null" json:"valid_till"`
HasExpirationDate bool `gorm:"default:false" json:"has_expiration_date"`
ReductionType string `gorm:"type:enum('amount','percentage');not null" json:"reduction_type"`
Price *float64 `gorm:"type:decimal(10,2);null" json:"price"`
CurrencyID *uint64 `gorm:"column:b2b_id_currency;null" json:"currency_id"`
PercentageReduction *float64 `gorm:"type:decimal(5,2);null" json:"percentage_reduction"`
FromQuantity uint32 `gorm:"default:1" json:"from_quantity"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt *time.Time `gorm:"null" json:"created_at"`
UpdatedAt *time.Time `gorm:"null" json:"updated_at"`
ProductIDs []uint64 `gorm:"-" json:"product_ids"`
CategoryIDs []uint64 `gorm:"-" json:"category_ids"`
ProductAttributeIDs []uint64 `gorm:"-" json:"product_attribute_ids"`
CountryIDs []uint64 `gorm:"-" json:"country_ids"`
CustomerIDs []uint64 `gorm:"-" json:"customer_ids"`
}
func (SpecificPrice) TableName() string {
return "b2b_specific_price"
}

View File

@@ -48,9 +48,9 @@ func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) {
func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error { func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error {
address := model.Address{ address := model.Address{
CustomerID: user_id, CustomerID: user_id,
AddressInfo: address_info, AddressString: address_info,
CountryID: country_id, CountryID: country_id,
} }
return db.DB. return db.DB.
@@ -60,10 +60,10 @@ func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, coun
func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error { func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error {
address := model.Address{ address := model.Address{
ID: address_id, ID: address_id,
CustomerID: user_id, CustomerID: user_id,
AddressInfo: address_info, AddressString: address_info,
CountryID: country_id, CountryID: country_id,
} }
return db.DB. return db.DB.

View File

@@ -1,20 +1,26 @@
package cartsRepo package cartsRepo
import ( import (
"errors"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"gorm.io/gorm"
) )
type UICartsRepo interface { type UICartsRepo interface {
CartsAmount(user_id uint) (uint, error) CartsAmount(user_id uint) (uint, error)
CreateNewCart(user_id uint) (model.CustomerCart, error) CreateNewCart(user_id uint, name string) (model.CustomerCart, error)
UserHasCart(user_id uint, cart_id uint) (uint, error) RemoveCart(user_id uint, cart_id uint) error
UserHasCart(user_id uint, cart_id uint) (bool, error)
UpdateCartName(user_id uint, cart_id uint, new_name string) error UpdateCartName(user_id uint, cart_id uint, new_name string) error
RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error)
RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error)
CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error)
AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error AddProduct(cart_id uint, product_id uint, product_attribute_id *uint, amount uint, set_amount bool) error
RemoveProduct(cart_id uint, product_id uint, product_attribute_id *uint) error
} }
type CartsRepo struct{} type CartsRepo struct{}
@@ -36,10 +42,7 @@ func (repo *CartsRepo) CartsAmount(user_id uint) (uint, error) {
return amt, err return amt, err
} }
func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) { func (repo *CartsRepo) CreateNewCart(user_id uint, name string) (model.CustomerCart, error) {
var name string
name = constdata.DEFAULT_NEW_CART_NAME
cart := model.CustomerCart{ cart := model.CustomerCart{
UserID: user_id, UserID: user_id,
Name: &name, Name: &name,
@@ -49,7 +52,15 @@ func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) {
return cart, err return cart, err
} }
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { func (repo *CartsRepo) RemoveCart(user_id uint, cart_id uint) error {
return db.DB.
Table("b2b_customer_carts").
Where("cart_id = ? AND user_id = ?", cart_id, user_id).
Delete(nil).
Error
}
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (bool, error) {
var amt uint var amt uint
err := db.DB. err := db.DB.
@@ -59,7 +70,7 @@ func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) {
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} }
func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error { func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
@@ -96,7 +107,7 @@ func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.Customer
return &cart, err return &cart, err
} }
func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) { func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error) {
var amt uint var amt uint
if product_attribute_id == nil { if product_attribute_id == nil {
@@ -106,7 +117,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id
Where("id_product = ?", product_id). Where("id_product = ?", product_id).
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} else { } else {
err := db.DB. err := db.DB.
@@ -116,18 +127,65 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id
Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id). Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id).
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} }
} }
func (repo *CartsRepo) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { func (repo *CartsRepo) AddProduct(cart_id uint, product_id uint, product_attribute_id *uint, amount uint, set_amount bool) error {
product := model.CartProduct{ var product model.CartProduct
CartID: cart_id,
ProductID: product_id,
ProductAttributeID: product_attribute_id,
Amount: amount,
}
err := db.DB.Create(&product).Error
return err err := db.DB.
Where(&model.CartProduct{
CartID: cart_id,
ProductID: product_id,
ProductAttributeID: product_attribute_id,
}).
First(&product).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if amount < 1 {
return responseErrors.ErrAmountMustBePositive
} else if amount > constdata.MAX_AMOUNT_OF_PRODUCT_IN_CART {
return responseErrors.ErrAmountMustBeReasonable
}
product = model.CartProduct{
CartID: cart_id,
ProductID: product_id,
ProductAttributeID: product_attribute_id,
Amount: amount,
}
return db.DB.Create(&product).Error
}
// Some other DB error
return err
}
// Product already exists in cart
if set_amount {
product.Amount = amount
} else {
product.Amount = product.Amount + amount
}
if product.Amount < 1 {
return responseErrors.ErrAmountMustBePositive
} else if product.Amount > constdata.MAX_AMOUNT_OF_PRODUCT_IN_CART {
return responseErrors.ErrAmountMustBeReasonable
}
return db.DB.Save(&product).Error
}
func (repo *CartsRepo) RemoveProduct(cart_id uint, product_id uint, product_attribute_id *uint) error {
return db.DB.
Where(&model.CartProduct{
CartID: cart_id,
ProductID: product_id,
ProductAttributeID: product_attribute_id,
}).
Delete(&model.CartProduct{}).Error
} }

View File

@@ -1,48 +0,0 @@
package categoriesRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type UICategoriesRepo interface {
GetAllCategories(idLang uint) ([]model.ScannedCategory, error)
}
type CategoriesRepo struct{}
func New() UICategoriesRepo {
return &CategoriesRepo{}
}
func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
categoryTbl := (&dbmodel.PsCategory{}).TableName()
categoryLangTbl := (&dbmodel.PsCategoryLang{}).TableName()
categoryShopTbl := (&dbmodel.PsCategoryShop{}).TableName()
langTbl := (&dbmodel.PsLang{}).TableName()
err := db.Get().
Model(dbmodel.PsCategory{}).
Select(`
ps_category.id_category AS category_id,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
`).
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
constdata.SHOP_ID).
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -2,11 +2,14 @@ package categoryrepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
) )
type UICategoryRepo interface { type UICategoryRepo interface {
GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error) GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error)
RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error)
} }
type CategoryRepo struct{} type CategoryRepo struct{}
@@ -42,3 +45,33 @@ func (r *CategoryRepo) GetCategoryTranslations(ids []uint, idLang uint) (map[uin
return translations, nil return translations, nil
} }
func (r *CategoryRepo) RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
categoryTbl := (&dbmodel.PsCategory{}).TableName()
categoryLangTbl := (&dbmodel.PsCategoryLang{}).TableName()
categoryShopTbl := (&dbmodel.PsCategoryShop{}).TableName()
langTbl := (&dbmodel.PsLang{}).TableName()
err := db.Get().
Model(dbmodel.PsCategory{}).
Select(`
ps_category.id_category AS category_id,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
`).
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
constdata.SHOP_ID).
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -1,6 +1,7 @@
package customerRepo package customerRepo
import ( import (
"fmt"
"strings" "strings"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
@@ -16,6 +17,7 @@ type UICustomerRepo interface {
Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error)
Save(customer *model.Customer) error Save(customer *model.Customer) error
Create(customer *model.Customer) error Create(customer *model.Customer) error
SetCustomerNoVatStatus(customerID uint, isNoVat bool) error
} }
type CustomerRepo struct{} type CustomerRepo struct{}
@@ -80,13 +82,16 @@ func (repo *CustomerRepo) Find(langId uint, p find.Paging, filt *filters.Filters
for _, word := range words { for _, word := range words {
conditions = append(conditions, ` conditions = append(conditions, `
(LOWER(first_name) LIKE ? OR (
id = ? OR
LOWER(first_name) LIKE ? OR
LOWER(last_name) LIKE ? OR LOWER(last_name) LIKE ? OR
LOWER(email) LIKE ?) LOWER(email) LIKE ?)
`) `)
args = append(args, strings.ToLower(word))
for range 3 { for range 3 {
args = append(args, "%"+strings.ToLower(word)+"%") args = append(args, fmt.Sprintf("%%%s%%", strings.ToLower(word)))
} }
} }
@@ -111,87 +116,6 @@ func (repo *CustomerRepo) Create(customer *model.Customer) error {
return db.DB.Create(customer).Error return db.DB.Create(customer).Error
} }
// func (repo *CustomerRepo) Search( func (repo *CustomerRepo) SetCustomerNoVatStatus(customerID uint, isNoVat bool) error {
// customerId uint, return db.DB.Model(&model.Customer{}).Where("id = ?", customerID).Update("is_no_vat", isNoVat).Error
// partnerCode string, }
// p find.Paging,
// filt *filters.FiltersList,
// search string,
// ) (found find.Found[model.UserInList], err error) {
// words := strings.Fields(search)
// if len(words) > 5 {
// words = words[:5]
// }
// query := ctx.DB().
// Model(&model.Customer{}).
// Select("customer.id AS id, customer.first_name as first_name, customer.last_name as last_name, customer.phone_number AS phone_number, customer.email AS email, count(distinct investment_plan_contract.id) as iiplan_purchases, count(distinct `order`.id) as single_purchases, entity.name as entity_name").
// Where("customer.id <> ?", customerId).
// Where("(customer.id IN (SELECT id FROM customer WHERE partner_code IN (WITH RECURSIVE partners AS (SELECT code AS dst FROM partner WHERE code = ? UNION SELECT code FROM partner JOIN partners ON partners.dst = partner.superior_code) SELECT dst FROM partners)) OR customer.recommender_code = ?)", partnerCode, partnerCode).
// Scopes(view.CustomerListQuery())
// var conditions []string
// var args []interface{}
// for _, word := range words {
// conditions = append(conditions, `
// (LOWER(first_name) LIKE ? OR
// LOWER(last_name) LIKE ? OR
// phone_number LIKE ? OR
// LOWER(email) LIKE ?)
// `)
// for i := 0; i < 4; i++ {
// args = append(args, "%"+strings.ToLower(word)+"%")
// }
// }
// finalQuery := strings.Join(conditions, " AND ")
// query = query.Where(finalQuery, args...).
// Scopes(filt.All()...)
// found, err = find.Paginate[V](ctx, p, query)
// return found, errs.Recorded(span, err)
// }
// func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) {
// var list []model.UserInList
// var total int64
// query := db.Get().
// Table("b2b_customers AS users").
// Select(`
// users.id AS id,
// users.email AS email,
// users.first_name AS first_name,
// users.last_name AS last_name,
// users.role AS role
// `)
// // Apply all filters
// if filt != nil {
// filt.ApplyAll(query)
// }
// // run counter first as query is without limit and offset
// err := query.Count(&total).Error
// if err != nil {
// return find.Found[model.UserInList]{}, err
// }
// err = query.
// Order("users.id DESC").
// Limit(p.Limit()).
// Offset(p.Offset()).
// Find(&list).Error
// if err != nil {
// return find.Found[model.UserInList]{}, err
// }
// return find.Found[model.UserInList]{
// Items: list,
// Count: uint(total),
// }, nil
// }

View File

@@ -3,6 +3,7 @@ package localeSelectorRepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
) )
type UILocaleSelectorRepo interface { type UILocaleSelectorRepo interface {
@@ -25,7 +26,9 @@ func (r *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
func (r *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) { func (r *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
var countries []model.Country var countries []model.Country
err := db.Get(). err := db.Get().
Preload("PSCurrency"). Select("*").
Preload("Currency").
Joins("LEFT JOIN " + dbmodel.TableNamePsCountryLang + " AS cl ON cl." + dbmodel.PsCountryLangCols.IDCountry.Col() + " = b2b_countries.ps_id_country AND cl." + dbmodel.PsCountryLangCols.IDLang.Col() + " = 2").
Find(&countries).Error Find(&countries).Error
return countries, err return countries, err
} }

View File

@@ -0,0 +1,122 @@
package ordersRepo
import (
"time"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UIOrdersRepo interface {
UserHasOrder(user_id uint, order_id uint) (bool, error)
Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error)
PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string, base_price float64, tax_incl float64, tax_excl float64) error
ChangeOrderAddress(order_id uint, country_id uint, address_info string) error
ChangeOrderStatus(order_id uint, status string) error
}
type OrdersRepo struct{}
func New() UIOrdersRepo {
return &OrdersRepo{}
}
func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error) {
var amt uint
err := db.DB.
Table("b2b_customer_orders").
Select("COUNT(*) AS amt").
Where("user_id = ? AND order_id = ?", user_id, order_id).
Scan(&amt).
Error
return amt >= 1, err
}
func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
var list []model.CustomerOrder
var total int64
query := db.Get().
Model(&model.CustomerOrder{}).
Preload("Products").
Order("b2b_customer_orders.order_id DESC")
// Apply all filters
if filt != nil {
filt.ApplyAll(query)
}
// run counter first as query is without limit and offset
err := query.Count(&total).Error
if err != nil {
return &find.Found[model.CustomerOrder]{}, err
}
err = query.
Limit(p.Limit()).
Offset(p.Offset()).
Find(&list).Error
if err != nil {
return &find.Found[model.CustomerOrder]{}, err
}
return &find.Found[model.CustomerOrder]{
Items: list,
Count: uint(total),
}, nil
}
func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string, base_price float64, tax_incl float64, tax_excl float64) error {
order := model.CustomerOrder{
UserID: cart.UserID,
Name: name,
CountryID: country_id,
AddressString: address_info,
Status: constdata.NEW_ORDER_STATUS,
Products: make([]model.OrderProduct, 0, len(cart.Products)),
}
for _, product := range cart.Products {
order.Products = append(order.Products, model.OrderProduct{
ProductID: product.ProductID,
ProductAttributeID: product.ProductAttributeID,
Amount: product.Amount,
})
}
order.CreatedAt = time.Now()
order.UpdatedAt = time.Now()
order.BasePrice = base_price
order.TaxIncl = tax_incl
order.TaxExcl = tax_excl
return db.DB.Create(&order).Error
}
func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, address_info string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Updates(map[string]interface{}{
"country_id": country_id,
"address_string": address_info,
"updated_at": time.Now(),
}).
Error
}
func (repo *OrdersRepo) ChangeOrderStatus(order_id uint, status string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Updates(map[string]interface{}{
"status": status,
"updated_at": time.Now(),
}).
Error
}

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -17,7 +16,6 @@ type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error) GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productid_lang uint) error CreateIfDoesNotExist(productID uint, productid_lang uint) error
UpdateFields(productID uint, productid_lang uint, updates map[string]string) error UpdateFields(productID uint, productid_lang uint, updates map[string]string) error
GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error)
} }
type ProductDescriptionRepo struct{} type ProductDescriptionRepo struct{}
@@ -118,108 +116,3 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productid_lang uin
return nil return nil
} }
// GetMeiliProductsBatchedScanned returns a batch of products with LIMIT/OFFSET pagination
// The scanning is done inside the repo to keep the service layer cleaner
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.Get().
Table("ps_product_shop ps").
Select(`
ps.id_product AS id_product,
pl.name AS name,
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
p.ean13,
p.reference,
ps.price,
ps.id_category_default AS id_category,
cl.name AS cat_name,
cl.link_rewrite AS l_rew,
COALESCE(vary.attributes, JSON_OBJECT()) AS attr,
COALESCE(feat.features, JSON_OBJECT()) AS feat,
img.id_image,
cat.category_ids,
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations
`, constdata.SHOP_ID).
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product").
Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product").
Joins("LEFT JOIN images img ON img.id_product = ps.id_product").
Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product").
Joins("JOIN products_page pp ON pp.id_product = ps.id_product").
Where("ps.active = ?", 1).
Order("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "products_page",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductShop{}).
Select("id_product, price").
Where("id_shop = ? AND active = 1", constdata.SHOP_ID).
Order("id_product").
Limit(limit).
Offset(offset),
},
},
{
Name: "variation_attributes",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_product_attribute_shop pas"). // <- explicit alias here
Select(`
pas.id_product,
pag.id_attribute_group AS attribute_name,
JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values
`).
Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute").
Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute").
Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group").
Where("pas.id_shop = ?", constdata.SHOP_ID).
Group("pas.id_product, pag.id_attribute_group"),
},
},
{
Name: "variations",
Subquery: exclause.Subquery{
DB: db.Get().
Table("variation_attributes").
Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes").
Group("id_product"),
},
},
{
Name: "features",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_feature_product pfp"). // <- explicit alias
Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features").
Group("pfp.id_product"),
},
},
{
Name: "images",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsImageShop{}).
Select("id_product, id_image").
Where("id_shop = ? AND cover = 1", constdata.SHOP_ID),
},
},
{
Name: "categories",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsCategoryProduct{}).
Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids").
Group("id_product"),
},
},
}}).Find(&products).Error
return products, err
}

View File

@@ -2,7 +2,6 @@ package productsRepo
import ( import (
"encoding/json" "encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
@@ -10,13 +9,18 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/view"
"git.ma-al.com/goc_marek/gormcol" "git.ma-al.com/goc_marek/gormcol"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
) )
type UIProductsRepo interface { type UIProductsRepo interface {
GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) // GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) Find(id_lang uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error)
GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error)
GetBase(p_id_product, p_id_shop, p_id_lang, p_id_customer uint) (view.Product, error)
GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error)
GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error)
AddToFavorites(userID uint, productID uint) error AddToFavorites(userID uint, productID uint) error
RemoveFromFavorites(userID uint, productID uint) error RemoveFromFavorites(userID uint, productID uint) error
ExistsInFavorites(userID uint, productID uint) (bool, error) ExistsInFavorites(userID uint, productID uint) (bool, error)
@@ -29,105 +33,238 @@ func New() UIProductsRepo {
return &ProductsRepo{} return &ProductsRepo{}
} }
func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) { func (repo *ProductsRepo) GetBase(p_id_product, p_id_shop, p_id_lang, p_id_customer uint) (view.Product, error) {
var productStr string // ← Scan as string first var result view.Product
err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`, err := db.DB.Raw(`CALL get_product_base(?,?,?,?)`,
p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity). p_id_product, p_id_shop, p_id_lang, p_id_customer).
Scan(&productStr). Scan(&result).Error
Error
if err != nil { return result, err
return nil, err
}
if !json.Valid([]byte(productStr)) {
return nil, fmt.Errorf("invalid json returned from stored procedure")
}
raw := json.RawMessage(productStr)
return &raw, nil
} }
func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { func (repo *ProductsRepo) GetPrice(
var list []model.ProductInList p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint,
var total int64 ) (view.Price, error) {
query := db.Get(). type row struct {
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps"). Price json.RawMessage `gorm:"column:price"`
Select(` }
ps.id_product AS product_id,
pl.name AS name, var r row
pl.link_rewrite AS link_rewrite, err := db.DB.Raw(`
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, SELECT fn_product_price(?,?,?,?,?,?) AS price`,
cl.name AS category_name, p_id_product, p_id_shop, p_id_customer, p_id_country, p_quantity, productAttributeID).
p.reference AS reference, Scan(&r).Error
COALESCE(v.variants_number, 0) AS variants_number,
sa.quantity AS quantity, if err != nil {
COALESCE(f.is_favorite, 0) AS is_favorite return view.Price{}, err
`, config.Get().Image.ImagePrefix). }
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang). var temp struct {
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1"). Base json.Number `json:"base"`
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang). FinalTaxExcl json.Number `json:"final_tax_excl"`
Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product"). FinalTaxIncl json.Number `json:"final_tax_incl"`
Joins("LEFT JOIN variants v ON v.id_product = ps.id_product"). TaxRate json.Number `json:"tax_rate"`
Joins("LEFT JOIN favorites f ON f.id_product = ps.id_product"). Priority json.Number `json:"priority"`
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0"). }
Where("ps.active = ?", 1).
Group("ps.id_product"). if err := json.Unmarshal(r.Price, &temp); err != nil {
return view.Price{}, err
}
price := view.Price{
Base: mustParseFloat(temp.Base),
FinalTaxExcl: mustParseFloat(temp.FinalTaxExcl),
FinalTaxIncl: mustParseFloat(temp.FinalTaxIncl),
TaxRate: mustParseFloat(temp.TaxRate),
Priority: mustParseInt(temp.Priority),
}
return price, nil
}
func mustParseFloat(n json.Number) float64 {
f, _ := n.Float64()
return f
}
func mustParseInt(n json.Number) int {
i, _ := n.Int64()
return int(i)
}
func (repo *ProductsRepo) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error) {
var results []view.ProductAttribute
err := db.DB.Raw(`
CALL get_product_variants(?,?,?,?,?,?)`,
p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity).
Scan(&results).Error
return results, err
}
func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
query := db.DB.
Table("base_products AS bp").
Clauses(exclause.With{ Clauses(exclause.With{
CTEs: []exclause.CTE{ CTEs: []exclause.CTE{
{
Name: "variants",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductAttributeShop{}).
Select("id_product", "COUNT(*) AS variants_number").
Group("id_product"),
},
},
{ {
Name: "favorites", Name: "favorites",
Subquery: exclause.Subquery{ Subquery: exclause.Subquery{
DB: db.Get(). DB: db.DB.
Table("b2b_favorites"). Table("b2b_favorites").
Select(` Select(`
product_id AS id_product, product_id AS product_id,
COUNT(*) > 0 AS is_favorite COUNT(*) > 0 AS is_favorite
`). `).
Where("user_id = ?", userID). Where("user_id = ?", userID).
Group("product_id"), Group("product_id"),
}, },
}, },
{
Name: "oems",
Subquery: exclause.Subquery{
DB: db.DB.
Table("b2b_oems").
Select(`
product_id AS product_id,
COUNT(*) > 0 AS is_customers_oem
`).
Where("user_id = ?", userID).
Group("product_id"),
},
},
{
Name: "new_product_days",
Subquery: exclause.Subquery{
DB: db.DB.
Table("ps_configuration").
Select("CAST(value AS SIGNED) AS days").
Where("name = ?", "PS_NB_DAYS_NEW_PRODUCT"),
},
},
{
Name: "variants",
Subquery: exclause.Subquery{
DB: db.DB.
Table("ps_product_attribute_shop").
Select("id_product, COUNT(*) AS variants_number").
Group("id_product"),
},
},
{
Name: "base_products",
Subquery: exclause.Subquery{
DB: db.DB.
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(`
ps.id_product AS product_id,
pl.name AS name,
ps.id_category_default AS category_id,
p.reference AS reference,
p.is_oem AS is_oem,
sa.quantity AS quantity,
COALESCE(f.is_favorite, 0) AS is_favorite,
CASE
WHEN ps.date_add >= DATE_SUB(
NOW(),
INTERVAL COALESCE(npd.days, 20) DAY
) AND ps.active = 1
THEN 1
ELSE 0
END AS is_new
`).
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", langID).
Joins("LEFT JOIN favorites f ON f.product_id = ps.id_product").
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0").
Joins("LEFT JOIN new_product_days npd ON 1 = 1").
Joins("LEFT JOIN oems ON oems.product_id = ps.id_product").
Where("ps.active = ?", 1).
Where("(p.is_oem = 0 OR oems.is_customers_oem > 0)").
Group("ps.id_product"),
},
},
}, },
}). }).
Order("ps.id_product DESC") Select(`
bp.product_id AS product_id,
bp.name AS name,
pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
cl.name AS category_name,
bp.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number,
bp.quantity AS quantity,
bp.is_favorite AS is_favorite,
bp.is_new AS is_new,
bp.is_oem AS is_oem
`, config.Get().Image.ImagePrefix).
Joins("JOIN ps_product_lang pl ON pl.id_product = bp.product_id AND pl.id_lang = ?", langID).
Joins("JOIN ps_image_shop ims ON ims.id_product = bp.product_id AND ims.cover = 1").
Joins("JOIN ps_category_lang cl ON cl.id_category = bp.category_id AND cl.id_lang = ?", langID).
Joins("LEFT JOIN variants v ON v.id_product = bp.product_id").
Order("bp.product_id DESC")
// Apply all filters query = query.Scopes(filt.All()...)
if filt != nil {
filt.ApplyAll(query)
}
// run counter first as query is without limit and offset list, err := find.Paginate[model.ProductInList](langID, p, query)
err := query.Count(&total).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return nil, err
} }
return &list, nil
}
func (repo *ProductsRepo) GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error) {
var result []view.ProductAttribute
err := db.DB.
Raw(`
CALL get_product_attributes_with_price(?, ?, ?, ?, ?, ?)
`,
langID,
productID,
shopID,
customerID,
countryID,
quantity,
).
Scan(&result).Error
err = query.
Limit(p.Limit()).
Offset(p.Offset()).
Find(&list).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return nil, err
} }
return find.Found[model.ProductInList]{ return result, nil
Items: list, }
Count: uint(total),
}, nil func (repo *ProductsRepo) PopulateProductPrice(product *model.ProductInList, targetCustomer *model.Customer, quantity int, shopID uint) error {
row := db.Get().Raw(
"CALL get_product_price(?, ?, ?, ?, ?)",
product.ProductID,
shopID,
targetCustomer.ID,
targetCustomer.CountryID,
quantity,
).Row()
var (
id uint
base float64
excl float64
incl float64
tax float64
)
err := row.Scan(&id, &base, &excl, &incl, &tax)
if err != nil {
return err
}
product.PriceTaxExcl = excl
product.PriceTaxIncl = incl
return nil
} }
func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error { func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error {

View File

@@ -7,7 +7,7 @@ import (
) )
type UIRoutesRepo interface { type UIRoutesRepo interface {
GetRoutes(langId uint) ([]model.Route, error) GetRoutes(langId uint, roleId uint) ([]model.Route, error)
GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error) GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
} }
@@ -17,13 +17,18 @@ func New() UIRoutesRepo {
return &RoutesRepo{} return &RoutesRepo{}
} }
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) { func (p *RoutesRepo) GetRoutes(langId uint, roleId uint) ([]model.Route, error) {
routes := []model.Route{} routes := []model.Route{}
err := db.DB.Find(&routes, model.Route{Active: nullable.GetNil(true)}).Error
if err != nil { err := db.
return nil, err Get().
} Model(model.Route{}).
return routes, nil Joins("JOIN b2b_route_roles rr ON rr.route_id = b2b_routes.id").
Where(model.Route{Active: nullable.GetNil(true)}).
Where("rr.role_id = ?", roleId).
Find(&routes).Error
return routes, err
} }
func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) { func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) {

View File

@@ -7,7 +7,11 @@ import (
"net/http" "net/http"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
) )
type SearchProxyResponse struct { type SearchProxyResponse struct {
@@ -17,6 +21,7 @@ type SearchProxyResponse struct {
type UISearchRepo interface { type UISearchRepo interface {
Search(index string, body []byte) (*SearchProxyResponse, error) Search(index string, body []byte) (*SearchProxyResponse, error)
GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error)
GetIndexSettings(index string) (*SearchProxyResponse, error) GetIndexSettings(index string) (*SearchProxyResponse, error)
GetRoutes(langId uint) ([]model.Route, error) GetRoutes(langId uint) ([]model.Route, error)
} }
@@ -80,3 +85,108 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
func (r *SearchRepo) GetRoutes(langId uint) ([]model.Route, error) { func (r *SearchRepo) GetRoutes(langId uint) ([]model.Route, error) {
return nil, nil return nil, nil
} }
// GetMeiliProductsProducts returns a batch of products with LIMIT/OFFSET pagination
// The scanning is done inside the repo to keep the service layer cleaner
func (r *SearchRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.Get().
Table("ps_product_shop ps").
Select(`
ps.id_product AS id_product,
pl.name AS name,
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
p.ean13,
p.reference,
ps.price,
ps.id_category_default AS id_category,
cl.name AS cat_name,
cl.link_rewrite AS l_rew,
COALESCE(vary.attributes, JSON_OBJECT()) AS attr,
COALESCE(feat.features, JSON_OBJECT()) AS feat,
img.id_image,
cat.category_ids,
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations
`, constdata.SHOP_ID).
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product").
Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product").
Joins("LEFT JOIN images img ON img.id_product = ps.id_product").
Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product").
Joins("JOIN products_page pp ON pp.id_product = ps.id_product").
Where("ps.active = ?", 1).
Order("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "products_page",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductShop{}).
Select("id_product, price").
Where("id_shop = ? AND active = 1", constdata.SHOP_ID).
Order("id_product").
Limit(limit).
Offset(offset),
},
},
{
Name: "variation_attributes",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_product_attribute_shop pas"). // <- explicit alias here
Select(`
pas.id_product,
pag.id_attribute_group AS attribute_name,
JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values
`).
Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute").
Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute").
Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group").
Where("pas.id_shop = ?", constdata.SHOP_ID).
Group("pas.id_product, pag.id_attribute_group"),
},
},
{
Name: "variations",
Subquery: exclause.Subquery{
DB: db.Get().
Table("variation_attributes").
Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes").
Group("id_product"),
},
},
{
Name: "features",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_feature_product pfp"). // <- explicit alias
Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features").
Group("pfp.id_product"),
},
},
{
Name: "images",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsImageShop{}).
Select("id_product, id_image").
Where("id_shop = ? AND cover = 1", constdata.SHOP_ID),
},
},
{
Name: "categories",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsCategoryProduct{}).
Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids").
Group("id_product"),
},
},
}}).Find(&products).Error
return products, err
}

View File

@@ -0,0 +1,247 @@
package specificPriceRepo
import (
"context"
"errors"
"time"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"gorm.io/gorm"
)
type UISpecificPriceRepo interface {
Create(ctx context.Context, pr *model.SpecificPrice) error
Update(ctx context.Context, pr *model.SpecificPrice) error
Delete(ctx context.Context, id uint64) error
GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error)
List(ctx context.Context) ([]*model.SpecificPrice, error)
SetActive(ctx context.Context, id uint64, active bool) error
}
type SpecificPriceRepo struct{}
func New() UISpecificPriceRepo {
return &SpecificPriceRepo{}
}
func (repo *SpecificPriceRepo) Create(ctx context.Context, pr *model.SpecificPrice) error {
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
pr.CreatedAt = &now
if err := tx.Create(pr).Error; err != nil {
return err
}
return repo.insertRelations(tx, pr)
})
}
func (repo *SpecificPriceRepo) Update(ctx context.Context, pr *model.SpecificPrice) error {
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
pr.UpdatedAt = &now
if err := tx.Save(pr).Error; err != nil {
return err
}
if err := repo.clearRelations(tx, pr.ID); err != nil {
return err
}
return repo.insertRelations(tx, pr)
})
}
func (repo *SpecificPriceRepo) GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error) {
var pr model.SpecificPrice
err := db.DB.WithContext(ctx).Where("id = ?", id).First(&pr).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
if err := repo.loadRelations(ctx, &pr); err != nil {
return nil, err
}
return &pr, nil
}
func (repo *SpecificPriceRepo) List(ctx context.Context) ([]*model.SpecificPrice, error) {
var specificPrices []*model.SpecificPrice
err := db.DB.WithContext(ctx).Find(&specificPrices).Error
if err != nil {
return nil, err
}
for i := range specificPrices {
if err := repo.loadRelations(ctx, specificPrices[i]); err != nil {
return nil, err
}
}
return specificPrices, nil
}
func (repo *SpecificPriceRepo) SetActive(ctx context.Context, id uint64, active bool) error {
return db.DB.WithContext(ctx).Model(&model.SpecificPrice{}).Where("id = ?", id).Update("is_active", active).Error
}
func (repo *SpecificPriceRepo) Delete(ctx context.Context, id uint64) error {
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("DELETE FROM b2b_specific_price_product WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_category WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_product_attribute WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_country WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_customer WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Delete(&model.SpecificPrice{}, "id = ?", id).Error; err != nil {
return err
}
return nil
})
}
func (repo *SpecificPriceRepo) insertRelations(tx *gorm.DB, pr *model.SpecificPrice) error {
if len(pr.ProductIDs) > 0 {
for _, productID := range pr.ProductIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_product (b2b_specific_price_id, id_product) VALUES (?, ?)
`, pr.ID, productID).Error; err != nil {
return err
}
}
}
if len(pr.CategoryIDs) > 0 {
for _, categoryID := range pr.CategoryIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_category (b2b_specific_price_id, id_category) VALUES (?, ?)
`, pr.ID, categoryID).Error; err != nil {
return err
}
}
}
if len(pr.ProductAttributeIDs) > 0 {
for _, attrID := range pr.ProductAttributeIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_product_attribute (b2b_specific_price_id, id_product_attribute) VALUES (?, ?)
`, pr.ID, attrID).Error; err != nil {
return err
}
}
}
if len(pr.CountryIDs) > 0 {
for _, countryID := range pr.CountryIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_country (b2b_specific_price_id, b2b_id_country) VALUES (?, ?)
`, pr.ID, countryID).Error; err != nil {
return err
}
}
}
if len(pr.CustomerIDs) > 0 {
for _, customerID := range pr.CustomerIDs {
if err := tx.Exec(`
INSERT INTO b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer) VALUES (?, ?)
`, pr.ID, customerID).Error; err != nil {
return err
}
}
}
return nil
}
func (repo *SpecificPriceRepo) clearRelations(tx *gorm.DB, id uint64) error {
if err := tx.Exec("DELETE FROM b2b_specific_price_product WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_category WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_product_attribute WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_country WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM b2b_specific_price_customer WHERE b2b_specific_price_id = ?", id).Error; err != nil {
return err
}
return nil
}
func (repo *SpecificPriceRepo) loadRelations(ctx context.Context, pr *model.SpecificPrice) error {
var err error
var productIDs []struct {
IDProduct uint64 `gorm:"column:id_product"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_product").Where("b2b_specific_price_id = ?", pr.ID).Select("id_product").Scan(&productIDs).Error; err != nil {
return err
}
for _, p := range productIDs {
pr.ProductIDs = append(pr.ProductIDs, p.IDProduct)
}
var categoryIDs []struct {
IDCategory uint64 `gorm:"column:id_category"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_category").Where("b2b_specific_price_id = ?", pr.ID).Select("id_category").Scan(&categoryIDs).Error; err != nil {
return err
}
for _, c := range categoryIDs {
pr.CategoryIDs = append(pr.CategoryIDs, c.IDCategory)
}
var attrIDs []struct {
IDAttr uint64 `gorm:"column:id_product_attribute"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_product_attribute").Where("b2b_specific_price_id = ?", pr.ID).Select("id_product_attribute").Scan(&attrIDs).Error; err != nil {
return err
}
for _, a := range attrIDs {
pr.ProductAttributeIDs = append(pr.ProductAttributeIDs, a.IDAttr)
}
var countryIDs []struct {
IDCountry uint64 `gorm:"column:b2b_id_country"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_country").Where("b2b_specific_price_id = ?", pr.ID).Select("b2b_id_country").Scan(&countryIDs).Error; err != nil {
return err
}
for _, c := range countryIDs {
pr.CountryIDs = append(pr.CountryIDs, c.IDCountry)
}
var customerIDs []struct {
IDCustomer uint64 `gorm:"column:b2b_id_customer"`
}
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_customer").Where("b2b_specific_price_id = ?", pr.ID).Select("b2b_id_customer").Scan(&customerIDs).Error; err != nil {
return err
}
for _, c := range customerIDs {
pr.CustomerIDs = append(pr.CustomerIDs, c.IDCustomer)
}
return nil
}

View File

@@ -21,7 +21,7 @@ func New() *AddressesService {
} }
} }
func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) { func (s *AddressesService) GetTemplate(country_id uint) (model.AddressUnparsed, error) {
switch country_id { switch country_id {
case 1: // Poland case 1: // Poland
@@ -49,7 +49,7 @@ func (s *AddressesService) AddNewAddress(user_id uint, address_info string, coun
return responseErrors.ErrMaxAmtOfAddressesReached return responseErrors.ErrMaxAmtOfAddressesReached
} }
_, err = s.validateAddressJson(address_info, country_id) _, err = s.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
} }
@@ -66,7 +66,7 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_
return responseErrors.ErrUserHasNoSuchAddress return responseErrors.ErrUserHasNoSuchAddress
} }
_, err = s.validateAddressJson(address_info, country_id) _, err = s.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
} }
@@ -74,30 +74,23 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_
return s.repo.UpdateAddress(user_id, address_id, address_info, country_id) return s.repo.UpdateAddress(user_id, address_id, address_info, country_id)
} }
func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) { func (s *AddressesService) RetrieveAddresses(user_id uint) (*[]model.Address, error) {
parsed_addresses, err := s.repo.RetrieveAddresses(user_id) addresses, err := s.repo.RetrieveAddresses(user_id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var unparsed_addresses []model.AddressUnparsed for i := 0; i < len(*addresses); i++ {
address_unparsed, err := s.ValidateAddressJson((*addresses)[i].AddressString, (*addresses)[i].CountryID)
for i := 0; i < len(*parsed_addresses); i++ {
var next_address model.AddressUnparsed
next_address.ID = (*parsed_addresses)[i].ID
next_address.CustomerID = (*parsed_addresses)[i].CustomerID
next_address.CountryID = (*parsed_addresses)[i].CountryID
next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID)
// log such errors // log such errors
if err != nil { if err != nil {
fmt.Printf("err: %v\n", err) fmt.Printf("err: %v\n", err)
} }
unparsed_addresses = append(unparsed_addresses, next_address) (*addresses)[i].AddressUnparsed = &address_unparsed
} }
return &unparsed_addresses, nil return addresses, nil
} }
func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
@@ -112,7 +105,7 @@ func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
} }
// validateAddressJson makes sure that the info string represents a valid json of address in given country // validateAddressJson makes sure that the info string represents a valid json of address in given country
func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) { func (s *AddressesService) ValidateAddressJson(info string, country_id uint) (model.AddressUnparsed, error) {
dec := json.NewDecoder(strings.NewReader(info)) dec := json.NewDecoder(strings.NewReader(info))
dec.DisallowUnknownFields() dec.DisallowUnknownFields()

View File

@@ -15,12 +15,14 @@ import (
roleRepo "git.ma-al.com/goc_daniel/b2b/app/repos/rolesRepo" roleRepo "git.ma-al.com/goc_daniel/b2b/app/repos/rolesRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService" "git.ma-al.com/goc_daniel/b2b/app/service/emailService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
// JWTClaims represents the JWT claims // JWTClaims represents the JWT claims
@@ -67,22 +69,47 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
// Find user by email // Find user by email
if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil { if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Info("login failed - invalid credentials",
"service", "AuthService.Login",
"email", req.Email,
"reason", "user not found",
)
return nil, "", responseErrors.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
logger.Error("login failed - database error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("database error: %w", err) return nil, "", fmt.Errorf("database error: %w", err)
} }
// Check if user is active // Check if user is active
if !user.IsActive { if !user.IsActive {
logger.Info("login failed - user inactive",
"service", "AuthService.Login",
"email", req.Email,
"reason", "user account is inactive",
)
return nil, "", responseErrors.ErrUserInactive return nil, "", responseErrors.ErrUserInactive
} }
// Check if email is verified // Check if email is verified
if !user.EmailVerified { if !user.EmailVerified {
logger.Info("login failed - email not verified",
"service", "AuthService.Login",
"email", req.Email,
"reason", "email not verified",
)
return nil, "", responseErrors.ErrEmailNotVerified return nil, "", responseErrors.ErrEmailNotVerified
} }
// Verify password // Verify password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
logger.Info("login failed - invalid credentials",
"service", "AuthService.Login",
"email", req.Email,
"reason", "wrong password",
)
return nil, "", responseErrors.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
@@ -93,22 +120,38 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
if req.LangID != nil { if req.LangID != nil {
_, err := s.GetLangISOCode(*req.LangID) _, err := s.GetLangISOCode(*req.LangID)
if err != nil { if err != nil {
logger.Warn("login failed - invalid language ID",
"service", "AuthService.Login",
"email", req.Email,
"reason", "invalid language ID",
)
return nil, "", responseErrors.ErrBadLangID return nil, "", responseErrors.ErrBadLangID
} }
user.LangID = *req.LangID user.LangID = *req.LangID
} }
user.Country = nil
s.db.Save(&user) s.db.Save(&user)
// Generate access token (JWT) // Generate access token (JWT)
accessToken, err := s.generateAccessToken(&user) accessToken, err := s.generateAccessToken(&user)
if err != nil { if err != nil {
logger.Error("login failed - token generation error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("failed to generate access token: %w", err) return nil, "", fmt.Errorf("failed to generate access token: %w", err)
} }
// Generate opaque refresh token and store in DB // Generate opaque refresh token and store in DB
rawRefreshToken, err := s.createRefreshToken(user.ID) rawRefreshToken, err := s.createRefreshToken(user.ID)
if err != nil { if err != nil {
logger.Error("login failed - refresh token creation error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("failed to create refresh token: %w", err) return nil, "", fmt.Errorf("failed to create refresh token: %w", err)
} }
@@ -169,6 +212,11 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
} }
if err := s.db.Create(&user).Error; err != nil { if err := s.db.Create(&user).Error; err != nil {
logger.Error("registration failed - database error",
"service", "AuthService.Register",
"email", req.Email,
"error", err.Error(),
)
return fmt.Errorf("failed to create user: %w", err) return fmt.Errorf("failed to create user: %w", err)
} }
@@ -180,8 +228,11 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
} }
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil { if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
// Log error but don't fail registration - user can request resend logger.Warn("failed to send verification email",
_ = err "service", "AuthService.Register",
"email", req.Email,
"error", err.Error(),
)
} }
return nil return nil
@@ -209,6 +260,7 @@ func (s *AuthService) CompleteRegistration(req *model.CompleteRegistrationReques
user.EmailVerificationToken = "" user.EmailVerificationToken = ""
user.EmailVerificationExpires = nil user.EmailVerificationExpires = nil
user.Country = nil
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(&user).Error; err != nil {
return nil, "", fmt.Errorf("failed to update user: %w", err) return nil, "", fmt.Errorf("failed to update user: %w", err)
} }
@@ -277,6 +329,7 @@ func (s *AuthService) RequestPasswordReset(emailAddr string) error {
user.PasswordResetToken = token user.PasswordResetToken = token
user.PasswordResetExpires = &expiresAt user.PasswordResetExpires = &expiresAt
user.LastPasswordResetRequest = &now user.LastPasswordResetRequest = &now
user.Country = nil
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(&user).Error; err != nil {
return fmt.Errorf("failed to save reset token: %w", err) return fmt.Errorf("failed to save reset token: %w", err)
} }
@@ -303,6 +356,10 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return responseErrors.ErrInvalidResetToken return responseErrors.ErrInvalidResetToken
} }
logger.Error("password reset failed - database error",
"service", "AuthService.ResetPassword",
"error", err.Error(),
)
return fmt.Errorf("database error: %w", err) return fmt.Errorf("database error: %w", err)
} }
@@ -327,7 +384,12 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
user.PasswordResetToken = "" user.PasswordResetToken = ""
user.PasswordResetExpires = nil user.PasswordResetExpires = nil
user.Country = nil
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(&user).Error; err != nil {
logger.Error("password reset failed - database error",
"service", "AuthService.ResetPassword",
"error", err.Error(),
)
return fmt.Errorf("failed to update password: %w", err) return fmt.Errorf("failed to update password: %w", err)
} }
@@ -436,7 +498,7 @@ func (s *AuthService) RevokeAllRefreshTokens(userID uint) {
// GetUserByID retrieves a user by ID // GetUserByID retrieves a user by ID
func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) { func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
var user model.Customer var user model.Customer
if err := s.db.Preload("Role.Permissions").First(&user, userID).Error; err != nil { if err := s.db.Preload("Role.Permissions").Preload(clause.Associations).First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, responseErrors.ErrUserNotFound return nil, responseErrors.ErrUserNotFound
} }
@@ -538,6 +600,7 @@ func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
} }
// Save the updated user // Save the updated user
user.Country = nil
if err := s.db.Save(user).Error; err != nil { if err := s.db.Save(user).Error; err != nil {
return "", fmt.Errorf("database error: %w", err) return "", fmt.Errorf("database error: %w", err)
} }

View File

@@ -8,10 +8,12 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/app/view" "git.ma-al.com/goc_daniel/b2b/app/view"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -77,12 +79,20 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
// Find or create user // Find or create user
user, err := s.findOrCreateGoogleUser(userInfo) user, err := s.findOrCreateGoogleUser(userInfo)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "database") {
logger.Error("google oauth callback failed - database error",
"service", "AuthService.HandleGoogleCallback",
"email", userInfo.Email,
"error", err.Error(),
)
}
return nil, "", err return nil, "", err
} }
// Update last login // Update last login
now := time.Now() now := time.Now()
user.LastLoginAt = &now user.LastLoginAt = &now
user.Country = nil
s.db.Save(user) s.db.Save(user)
// Generate access token (JWT) // Generate access token (JWT)

View File

@@ -4,6 +4,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
@@ -17,7 +18,7 @@ func New() *CartsService {
} }
} }
func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) { func (s *CartsService) CreateNewCart(user_id uint, name string) (model.CustomerCart, error) {
var cart model.CustomerCart var cart model.CustomerCart
customers_carts_amount, err := s.repo.CartsAmount(user_id) customers_carts_amount, err := s.repo.CartsAmount(user_id)
@@ -28,18 +29,43 @@ func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) {
return cart, responseErrors.ErrMaxAmtOfCartsReached return cart, responseErrors.ErrMaxAmtOfCartsReached
} }
if name == "" {
name = constdata.DEFAULT_NEW_CART_NAME
}
// create new cart for customer // create new cart for customer
cart, err = s.repo.CreateNewCart(user_id) cart, err = s.repo.CreateNewCart(user_id, name)
if err != nil {
return cart, err
}
logger.Info("cart created",
"service", "cartsService",
"user_id", user_id,
"cart_id", cart.CartID,
)
return cart, nil return cart, nil
} }
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { func (s *CartsService) RemoveCart(user_id uint, cart_id uint) error {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
return s.repo.RemoveCart(user_id, cart_id)
}
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart return responseErrors.ErrUserHasNoSuchCart
} }
@@ -51,33 +77,45 @@ func (s *CartsService) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, er
} }
func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if amt != 1 { if !exists {
return nil, responseErrors.ErrUserHasNoSuchCart return nil, responseErrors.ErrUserHasNoSuchCart
} }
return s.repo.RetrieveCart(user_id, cart_id) return s.repo.RetrieveCart(user_id, cart_id)
} }
func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount int, set_amount bool) error {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrUserHasNoSuchCart return responseErrors.ErrUserHasNoSuchCart
} }
amt, err = s.repo.CheckProductExists(product_id, product_attribute_id) exists, err = s.repo.CheckProductExists(product_id, product_attribute_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrProductOrItsVariationDoesNotExist return responseErrors.ErrProductOrItsVariationDoesNotExist
} }
return s.repo.AddProduct(user_id, cart_id, product_id, product_attribute_id, amount) return s.repo.AddProduct(cart_id, product_id, product_attribute_id, uint(amount), set_amount)
}
func (s *CartsService) RemoveProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint) error {
exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
return s.repo.RemoveProduct(cart_id, product_id, product_attribute_id)
} }

View File

@@ -24,3 +24,7 @@ func (s *CustomerService) GetById(id uint) (*model.Customer, error) {
func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) { func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) {
return s.repo.Find(langId, p, filt, search) return s.repo.Find(langId, p, filt, search)
} }
func (s *CustomerService) SetCustomerNoVatStatus(customerID uint, isNoVat bool) error {
return s.repo.SetCustomerNoVatStatus(customerID, isNoVat)
}

View File

@@ -117,6 +117,18 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL
return s.SendEmail(s.config.AdminEmail, subject, body) return s.SendEmail(s.config.AdminEmail, subject, body)
} }
// SendNewOrderPlacedNotification sends an email to admin when new order is placed
func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error {
if s.config.AdminEmail == "" {
return nil // No admin email configured
}
subject := "New Order Created"
body := s.newOrderPlacedTemplate(userID)
return s.SendEmail(s.config.AdminEmail, subject, body)
}
// verificationEmailTemplate returns the HTML template for email verification // verificationEmailTemplate returns the HTML template for email verification
func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string { func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string {
buf := bytes.Buffer{} buf := bytes.Buffer{}
@@ -137,3 +149,10 @@ func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, bas
emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf)
return buf.String() return buf.String()
} }
// newUserAdminNotificationTemplate returns the HTML template for admin notification
func (s *EmailService) newOrderPlacedTemplate(userID uint) string {
buf := bytes.Buffer{}
emails.EmailNewOrderPlacedWrapper(view.EmailLayout[view.EmailNewOrderPlacedData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailNewOrderPlacedData{UserID: userID}}).Render(context.Background(), &buf)
return buf.String()
}

View File

@@ -6,7 +6,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo" searchrepo "git.ma-al.com/goc_daniel/b2b/app/repos/searchRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
) )
@@ -20,8 +20,8 @@ type MeiliIndexSettings struct {
} }
type MeiliService struct { type MeiliService struct {
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo searchRepo searchrepo.UISearchRepo
meiliClient meilisearch.ServiceManager meiliClient meilisearch.ServiceManager
} }
func New() *MeiliService { func New() *MeiliService {
@@ -32,8 +32,8 @@ func New() *MeiliService {
) )
return &MeiliService{ return &MeiliService{
meiliClient: client, meiliClient: client,
productDescriptionRepo: productDescriptionRepo.New(), searchRepo: searchrepo.New(),
} }
} }
@@ -50,7 +50,7 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
for { for {
// Get batch of products from repo (includes scanning) // Get batch of products from repo (includes scanning)
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang, offset, batchSize) products, err := s.searchRepo.GetMeiliProducts(id_lang, offset, batchSize)
if err != nil { if err != nil {
return fmt.Errorf("failed to get products batch at offset %d: %w", offset, err) return fmt.Errorf("failed to get products batch at offset %d: %w", offset, err)
} }

View File

@@ -3,31 +3,45 @@ package menuService
import ( import (
"slices" "slices"
"sort" "sort"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo" categoryrepo "git.ma-al.com/goc_daniel/b2b/app/repos/categoryRepo"
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo" routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
type MenuService struct { type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo categoryRepo categoryrepo.UICategoryRepo
routesRepo routesRepo.UIRoutesRepo routesRepo routesRepo.UIRoutesRepo
} }
func New() *MenuService { func New() *MenuService {
return &MenuService{ return &MenuService{
categoriesRepo: categoriesRepo.New(), categoryRepo: categoryrepo.New(),
routesRepo: routesRepo.New(), routesRepo: routesRepo.New(),
} }
} }
func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) { func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoryRepo.RetrieveMenuCategories(id_lang)
if err != nil { if err != nil {
return &model.Category{}, err return &model.Category{}, err
} }
// remove blacklisted categories
// to do so, we detach them from the main tree
for i := 0; i < len(all_categories); i++ {
if slices.Contains(constdata.CATEGORY_BLACKLIST, all_categories[i].CategoryID) {
all_categories[i].ParentID = all_categories[i].CategoryID
}
}
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
// find the root // find the root
root_index := 0 root_index := 0
root_found := false root_found := false
@@ -88,8 +102,8 @@ func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCate
return node, true return node, true
} }
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) { func (s *MenuService) GetRoutes(id_lang, roleId uint) ([]model.Route, error) {
return s.routesRepo.GetRoutes(id_lang) return s.routesRepo.GetRoutes(id_lang, roleId)
} }
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category { func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
@@ -98,7 +112,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
normal.CategoryID = scanned.CategoryID normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name normal.Label = scanned.Name
// normal.Active = scanned.Active == 1 // normal.Active = scanned.Active == 1
normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode, Filter: scanned.Filter}
normal.Children = []model.Category{} normal.Children = []model.Category{}
return normal return normal
} }
@@ -114,11 +128,14 @@ func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position } func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) { func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoryRepo.RetrieveMenuCategories(id_lang)
if err != nil { if err != nil {
return []model.CategoryInBreadcrumb{}, err return []model.CategoryInBreadcrumb{}, err
} }
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
breadcrumb := []model.CategoryInBreadcrumb{} breadcrumb := []model.CategoryInBreadcrumb{}
start_index := 0 start_index := 0
@@ -211,3 +228,57 @@ func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopM
return roots, nil return roots, nil
} }
func (s *MenuService) appendAdditional(all_categories *[]model.ScannedCategory, id_lang uint, iso_code string) {
for i := 0; i < len(*all_categories); i++ {
(*all_categories)[i].Filter = "category_id_eq=" + strconv.Itoa(int((*all_categories)[i].CategoryID))
}
// the new products category
var new_products_category model.ScannedCategory
new_products_category.CategoryID = constdata.ADDITIONAL_CATEGORIES_INDEX + 1
new_products_category.Name = "New Products"
new_products_category.Active = 1
new_products_category.Position = 10
new_products_category.ParentID = 2
new_products_category.IsRoot = 0
new_products_category.LinkRewrite = i18n.T___(id_lang, "category.new_products")
new_products_category.IsoCode = iso_code
new_products_category.Visited = false
new_products_category.Filter = "is_new_eq=true"
*all_categories = append(*all_categories, new_products_category)
// the oem products category
var oem_products_category model.ScannedCategory
oem_products_category.CategoryID = constdata.ADDITIONAL_CATEGORIES_INDEX + 2
oem_products_category.Name = "OEM Products"
oem_products_category.Active = 1
oem_products_category.Position = 11
oem_products_category.ParentID = 2
oem_products_category.IsRoot = 0
oem_products_category.LinkRewrite = i18n.T___(id_lang, "category.oem_products")
oem_products_category.IsoCode = iso_code
oem_products_category.Visited = false
oem_products_category.Filter = "is_oem_eq=true"
*all_categories = append(*all_categories, oem_products_category)
// the favorite products category
var favorite_products_category model.ScannedCategory
favorite_products_category.CategoryID = constdata.ADDITIONAL_CATEGORIES_INDEX + 3
favorite_products_category.Name = "Favourite Products" // British English version.
favorite_products_category.Active = 1
favorite_products_category.Position = 12
favorite_products_category.ParentID = 2
favorite_products_category.IsRoot = 0
favorite_products_category.LinkRewrite = i18n.T___(id_lang, "category.favorite_products")
favorite_products_category.IsoCode = iso_code
favorite_products_category.Visited = false
favorite_products_category.Filter = "is_favorite_eq=true"
*all_categories = append(*all_categories, favorite_products_category)
}

View File

@@ -0,0 +1,185 @@
package orderService
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
"git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo"
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type OrderService struct {
ordersRepo ordersRepo.UIOrdersRepo
cartsRepo cartsRepo.UICartsRepo
productsRepo productsRepo.UIProductsRepo
addressesService *addressesService.AddressesService
emailService *emailService.EmailService
}
func New() *OrderService {
return &OrderService{
ordersRepo: ordersRepo.New(),
cartsRepo: cartsRepo.New(),
productsRepo: productsRepo.New(),
addressesService: addressesService.New(),
emailService: emailService.NewEmailService(),
}
}
func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
if !user.HasPermission(perms.OrdersViewAll) {
// append filter to view only this user's orders
idStr := strconv.FormatUint(uint64(user.ID), 10)
filt.Append(filters.Where("b2b_customer_orders.user_id = " + idStr))
}
list, err := s.ordersRepo.Find(user.ID, p, filt)
if err != nil {
return nil, err
}
for i := 0; i < len(list.Items); i++ {
address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID)
if err != nil {
logger.Warn("failed to validate address",
"service", "orderService",
"order_id", list.Items[i].OrderID,
"error", err.Error(),
)
}
list.Items[i].AddressUnparsed = &address_unparsed
}
return list, nil
}
func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil {
return err
}
exists, err := s.cartsRepo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
cart, err := s.cartsRepo.RetrieveCart(user_id, cart_id)
if err != nil {
return err
}
if len(cart.Products) == 0 {
return responseErrors.ErrEmptyCart
}
if name == "" && cart.Name != nil {
name = *cart.Name
}
base_price, tax_incl, tax_excl, err := s.getOrderTotalPrice(user_id, cart_id, country_id)
// all checks passed
err = s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info, base_price, tax_incl, tax_excl)
if err != nil {
return err
}
// from this point onward we do not cancel this order.
// if no error is returned, remove the cart. This should be smooth
err = s.cartsRepo.RemoveCart(user_id, cart_id)
if err != nil {
logger.Warn("failed to remove cart after order placement",
"service", "orderService",
"user_id", user_id,
"cart_id", cart_id,
"error", err.Error(),
)
}
// send email to admin
go func(user_id uint) {
err := s.emailService.SendNewOrderPlacedNotification(user_id)
if err != nil {
logger.Warn("failed to send new order notification",
"service", "orderService",
"user_id", user_id,
"error", err.Error(),
)
}
}(user_id)
return nil
}
func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil {
return err
}
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info)
}
// This is obiously just an initial version of this function
func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error {
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
return s.ordersRepo.ChangeOrderStatus(order_id, status)
}
func (s *OrderService) getOrderTotalPrice(user_id uint, cart_id uint, country_id uint) (float64, float64, float64, error) {
cart, err := s.cartsRepo.RetrieveCart(user_id, cart_id)
if err != nil {
return 0.0, 0.0, 0.0, err
}
base_price := 0.0
tax_incl := 0.0
tax_excl := 0.0
for _, product := range cart.Products {
prices, err := s.productsRepo.GetPrice(product.ProductID, product.ProductAttributeID, constdata.SHOP_ID, user_id, country_id, product.Amount)
if err != nil {
return 0.0, 0.0, 0.0, err
}
base_price += prices.Base
tax_incl += prices.FinalTaxIncl
tax_excl += prices.FinalTaxExcl
}
return base_price, tax_incl, tax_excl, nil
}

View File

@@ -2,6 +2,7 @@ package productService
import ( import (
"encoding/json" "encoding/json"
"errors"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
@@ -9,6 +10,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/app/view"
) )
type ProductService struct { type ProductService struct {
@@ -21,17 +23,108 @@ func New() *ProductService {
} }
} }
func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) { func (s *ProductService) Get(
products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity) p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity uint,
) (*json.RawMessage, error) {
product, err := s.productsRepo.GetBase(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer)
if err != nil { if err != nil {
return products, err return nil, err
} }
return products, nil price, err := s.productsRepo.GetPrice(p_id_product, nil, constdata.SHOP_ID, p_id_customer, b2b_id_country, p_quantity)
if err != nil {
return nil, err
}
variants, err := s.productsRepo.GetVariants(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
if err != nil {
return nil, err
}
result := view.ProductFull{
Product: product,
Price: price,
Variants: variants,
}
if len(variants) > 0 {
result.Variants = variants
}
jsonBytes, err := json.Marshal(result)
if err != nil {
return nil, err
}
raw := json.RawMessage(jsonBytes)
return &raw, nil
} }
func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { func (s *ProductService) Find(
return s.productsRepo.Find(id_lang, userID, p, filters) idLang uint,
userID uint,
p find.Paging,
filters *filters.FiltersList,
customer *model.Customer,
quantity uint,
shopID uint,
) (*find.Found[model.ProductInList], error) {
if customer == nil || customer.Country == nil {
return nil, errors.New("customer is nil or missing fields")
}
found, err := s.productsRepo.Find(idLang, userID, p, filters)
if err != nil {
return nil, err
}
// 1. collect simple products (no variants)
simpleProductIndexes := make([]int, 0, len(found.Items))
for i := range found.Items {
if found.Items[i].VariantsNumber <= 0 {
simpleProductIndexes = append(simpleProductIndexes, i)
}
}
// 2. resolve prices ONLY for simple products
for _, i := range simpleProductIndexes {
price, err := s.productsRepo.GetPrice(
found.Items[i].ProductID,
nil,
shopID,
customer.ID,
customer.CountryID,
quantity,
)
if err != nil {
return nil, err
}
found.Items[i].PriceTaxExcl = price.FinalTaxExcl
found.Items[i].PriceTaxIncl = price.FinalTaxIncl
}
return found, nil
}
func (s *ProductService) GetProductAttributes(
langID uint,
productID uint,
shopID uint,
customerID uint,
countryID uint,
quantity uint,
) ([]view.ProductAttribute, error) {
variants, err := s.productsRepo.GetVariants(productID, constdata.SHOP_ID, langID, customerID, countryID, quantity)
if err != nil {
return nil, err
}
return variants, nil
} }
func (s *ProductService) AddToFavorites(userID uint, productID uint) error { func (s *ProductService) AddToFavorites(userID uint, productID uint) error {

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"unicode" "unicode"
"git.ma-al.com/goc_daniel/b2b/app/db"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"golang.org/x/text/runes" "golang.org/x/text/runes"
@@ -22,7 +23,7 @@ func SanitizeSlug(s string) string {
s = strings.TrimSpace(strings.ToLower(s)) s = strings.TrimSpace(strings.ToLower(s))
// First apply explicit transliteration for language-specific letters. // First apply explicit transliteration for language-specific letters.
s = transliterateWithTable(s) s = transliterateSlug(s)
// Then normalize and strip any remaining combining marks. // Then normalize and strip any remaining combining marks.
s = removeDiacritics(s) s = removeDiacritics(s)
@@ -40,19 +41,17 @@ func SanitizeSlug(s string) string {
return s return s
} }
func transliterateWithTable(s string) string { func transliterateSlug(s string) string {
var b strings.Builder var cleared string
b.Grow(len(s))
for _, r := range s { err := db.DB.Raw("SELECT slugify_eu(?)", s).Scan(&cleared).Error
if repl, ok := constdata.TRANSLITERATION_TABLE[r]; ok { if err != nil {
b.WriteString(repl) // log error
} else { _ = err
b.WriteRune(r) return s
}
} }
return b.String() return cleared
} }
func removeDiacritics(s string) string { func removeDiacritics(s string) string {

View File

@@ -0,0 +1,124 @@
package specificPriceService
import (
"context"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/specificPriceRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type SpecificPriceService struct {
specificPriceRepo specificPriceRepo.UISpecificPriceRepo
}
func New() *SpecificPriceService {
return &SpecificPriceService{
specificPriceRepo: specificPriceRepo.New(),
}
}
func (s *SpecificPriceService) Create(ctx context.Context, pr *model.SpecificPrice) (*model.SpecificPrice, error) {
if err := s.validateRequest(pr); err != nil {
return nil, err
}
if err := s.specificPriceRepo.Create(ctx, pr); err != nil {
return nil, err
}
return pr, nil
}
func (s *SpecificPriceService) Update(ctx context.Context, id uint64, pr *model.SpecificPrice) (*model.SpecificPrice, error) {
existing, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if existing == nil {
return nil, responseErrors.ErrSpecificPriceNotFound
}
if err := s.validateUpdateRequest(pr); err != nil {
return nil, err
}
pr.ID = id
if err := s.specificPriceRepo.Update(ctx, pr); err != nil {
return nil, err
}
return pr, nil
}
func (s *SpecificPriceService) GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error) {
pr, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if pr == nil {
return nil, responseErrors.ErrSpecificPriceNotFound
}
return pr, nil
}
func (s *SpecificPriceService) List(ctx context.Context) ([]*model.SpecificPrice, error) {
return s.specificPriceRepo.List(ctx)
}
func (s *SpecificPriceService) SetActive(ctx context.Context, id uint64, active bool) error {
pr, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return err
}
if pr == nil {
return responseErrors.ErrSpecificPriceNotFound
}
return s.specificPriceRepo.SetActive(ctx, id, active)
}
func (s *SpecificPriceService) Delete(ctx context.Context, id uint64) error {
pr, err := s.specificPriceRepo.GetByID(ctx, id)
if err != nil {
return err
}
if pr == nil {
return responseErrors.ErrSpecificPriceNotFound
}
return s.specificPriceRepo.Delete(ctx, id)
}
func (s *SpecificPriceService) validateRequest(pr *model.SpecificPrice) error {
if pr.ReductionType != "amount" && pr.ReductionType != "percentage" {
return responseErrors.ErrInvalidReductionType
}
if pr.ReductionType == "percentage" && pr.PercentageReduction == nil {
return responseErrors.ErrPercentageRequired
}
if pr.ReductionType == "amount" && pr.Price == nil {
return responseErrors.ErrPriceRequired
}
return nil
}
func (s *SpecificPriceService) validateUpdateRequest(pr *model.SpecificPrice) error {
if pr.ReductionType != "" && pr.ReductionType != "amount" && pr.ReductionType != "percentage" {
return responseErrors.ErrInvalidReductionType
}
if pr.ReductionType == "percentage" && pr.PercentageReduction == nil {
return responseErrors.ErrPercentageRequired
}
if pr.ReductionType == "amount" && pr.Price == nil {
return responseErrors.ErrPriceRequired
}
return nil
}

View File

@@ -0,0 +1,26 @@
package emails
import (
"git.ma-al.com/goc_daniel/b2b/app/templ/layout"
"git.ma-al.com/goc_daniel/b2b/app/view"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
)
templ EmailNewOrderPlacedWrapper(data view.EmailLayout[view.EmailNewOrderPlacedData]) {
@layout.Base( i18n.T___(data.LangID, "email.email_new_order_placed_notification_title")) {
<div class="container">
<div class="email-wrapper">
<div class="email-header">
<h1>New Order Placed</h1>
</div>
<div class="email-body">
<p>Hello Administrator,</p>
<p>User with id { data.Data.UserID } has placed a new order. </p>
</div>
<div class="email-footer">
<p>&copy; 2024 Gitea Manager. All rights reserved.</p>
</div>
</div>
</div>
}
}

View File

@@ -3,19 +3,28 @@ package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
const SHOP_ID = 1 const SHOP_ID = 1
const DEFAULT_PRODUCT_QUANTITY = 1
const SHOP_DEFAULT_LANGUAGE = 1 const SHOP_DEFAULT_LANGUAGE = 1
const ADMIN_NOTIFICATION_LANGUAGE = 2 const ADMIN_NOTIFICATION_LANGUAGE = 2
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1 // CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
const CATEGORY_TREE_ROOT_ID = 2 const CATEGORY_TREE_ROOT_ID = 2
const ADDITIONAL_CATEGORIES_INDEX = 10000
// since arrays can not be const
var CATEGORY_BLACKLIST = []uint{250}
const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const MAX_AMOUNT_OF_CARTS_PER_USER = 10
const DEFAULT_NEW_CART_NAME = "new cart" const DEFAULT_NEW_CART_NAME = "new cart"
const MAX_AMOUNT_OF_PRODUCT_IN_CART = 1024
const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10 const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10
const USER_LOCALE = "user" const USER_LOCALE = "user"
// ORDERS
const NEW_ORDER_STATUS = "PENDING"
// WEBDAV // WEBDAV
const NBYTES_IN_WEBDAV_TOKEN = 32 const NBYTES_IN_WEBDAV_TOKEN = 32
const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage" const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage"
@@ -26,22 +35,4 @@ const NON_ALNUM_REGEX = `[^a-z0-9]+`
const MULTI_DASH_REGEX = `-+` const MULTI_DASH_REGEX = `-+`
const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$` const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$`
// Currently supports only German+Polish specific cases const UNLOGGED_USER_ROLE_ID = 4
var TRANSLITERATION_TABLE = map[rune]string{
// German
'ä': "ae",
'ö': "oe",
'ü': "ue",
'ß': "ss",
// Polish
'ą': "a",
'ć': "c",
'ę': "e",
'ł': "l",
'ń': "n",
'ó': "o",
'ś': "s",
'ż': "z",
'ź': "z",
}

View File

@@ -22,14 +22,6 @@ func GetUserID(c fiber.Ctx) (uint, bool) {
return user_locale.User.ID, true return user_locale.User.ID, true
} }
func GetOriginalUserRole(c fiber.Ctx) (model.Role, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil || user_locale.OriginalUser.Role == nil {
return model.Role{}, false
}
return *user_locale.OriginalUser.Role, true
}
func GetCustomer(c fiber.Ctx) (*model.Customer, bool) { func GetCustomer(c fiber.Ctx) (*model.Customer, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.User == nil { if !ok || user_locale.User == nil {

151
app/utils/logger/logger.go Normal file
View File

@@ -0,0 +1,151 @@
package logger
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"strings"
)
var L *slog.Logger
const (
reset = "\033[0m"
red = "\033[31m"
yellow = "\033[33m"
green = "\033[32m"
blue = "\033[36m"
gray = "\033[90m"
)
type consoleHandler struct {
w io.Writer
colorize bool
}
func (h *consoleHandler) Enabled(ctx context.Context, level slog.Level) bool {
return true
}
func (h *consoleHandler) Handle(ctx context.Context, r slog.Record) error {
level := r.Level.String()
color := reset
switch r.Level {
case slog.LevelError:
color = red
case slog.LevelWarn:
color = yellow
case slog.LevelInfo:
color = blue
case slog.LevelDebug:
color = gray
}
var msg string
if h.colorize {
msg = fmt.Sprintf("%s%s%s %s%s%s %s%s%s",
reset, r.Time.Format("15:04:05"), reset,
color, level, reset,
reset, r.Message, reset)
} else {
msg = fmt.Sprintf("%s %s %s", r.Time.Format("15:04:05"), level, r.Message)
}
var pairs []string
r.Attrs(func(attr slog.Attr) bool {
if h.colorize {
pairs = append(pairs, fmt.Sprintf("%s%s=%s%v%s", green, attr.Key, reset, attr.Value, reset))
} else {
pairs = append(pairs, fmt.Sprintf("%s=%v", attr.Key, attr.Value))
}
return true
})
if len(pairs) > 0 {
msg += " " + strings.Join(pairs, " ")
}
fmt.Fprintln(h.w, msg)
return nil
}
func (h *consoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &consoleHandler{w: h.w, colorize: h.colorize}
}
func (h *consoleHandler) WithGroup(name string) slog.Handler {
return &consoleHandler{w: h.w, colorize: h.colorize}
}
func Init(service string, output io.Writer, level string, colorize bool) {
if output == nil {
output = os.Stderr
}
var lvl slog.Level
switch level {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
if colorize {
L = slog.New(&consoleHandler{w: output, colorize: true}).With("service", service, "level", lvl.String())
} else {
L = slog.New(slog.NewJSONHandler(output, &slog.HandlerOptions{
Level: lvl,
AddSource: false,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && groups == nil {
a.Key = "timestamp"
}
if a.Key == slog.LevelKey {
a.Key = "level"
}
if a.Key == slog.MessageKey {
a.Key = "message"
}
return a
},
})).With("service", service)
}
}
func Info(msg string, args ...any) {
if L != nil {
L.Info(msg, args...)
}
}
func Warn(msg string, args ...any) {
if L != nil {
L.Warn(msg, args...)
}
}
func Error(msg string, args ...any) {
if L != nil {
L.Error(msg, args...)
}
}
func Debug(msg string, args ...any) {
if L != nil {
L.Debug(msg, args...)
}
}
func With(args ...any) *slog.Logger {
if L == nil {
return nil
}
return L.With(args...)
}

View File

@@ -65,6 +65,18 @@ var (
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
ErrAmountMustBePositive = errors.New("amount must be positive")
ErrAmountMustBeReasonable = errors.New("amount must be reasonable")
// Typed errors for orders handler
ErrEmptyCart = errors.New("the cart is empty")
ErrUserHasNoSuchOrder = errors.New("user does not have order with given id")
// Typed errors for price reduction handler
ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'")
ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage")
ErrPriceRequired = errors.New("price required when reduction_type is amount")
ErrSpecificPriceNotFound = errors.New("price reduction not found")
// Typed errors for storage // Typed errors for storage
ErrAccessDenied = errors.New("access denied!") ErrAccessDenied = errors.New("access denied!")
@@ -195,6 +207,15 @@ func GetErrorCode(c fiber.Ctx, err error) string {
return i18n.T_(c, "error.err_user_has_no_such_cart") return i18n.T_(c, "error.err_user_has_no_such_cart")
case errors.Is(err, ErrProductOrItsVariationDoesNotExist): case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist") return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist")
case errors.Is(err, ErrAmountMustBePositive):
return i18n.T_(c, "error.err_amount_must_be_positive")
case errors.Is(err, ErrAmountMustBeReasonable):
return i18n.T_(c, "error.err_amount_must_be_reasonable")
case errors.Is(err, ErrEmptyCart):
return i18n.T_(c, "error.err_cart_is_empty")
case errors.Is(err, ErrUserHasNoSuchOrder):
return i18n.T_(c, "error.err_user_has_no_such_order")
case errors.Is(err, ErrAccessDenied): case errors.Is(err, ErrAccessDenied):
return i18n.T_(c, "error.err_access_denied") return i18n.T_(c, "error.err_access_denied")
@@ -207,6 +228,15 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrMissingFileFieldDocument): case errors.Is(err, ErrMissingFileFieldDocument):
return i18n.T_(c, "error.err_missing_file_field_document") return i18n.T_(c, "error.err_missing_file_field_document")
case errors.Is(err, ErrInvalidReductionType):
return i18n.T_(c, "error.invalid_reduction_type")
case errors.Is(err, ErrPercentageRequired):
return i18n.T_(c, "error.percentage_required")
case errors.Is(err, ErrPriceRequired):
return i18n.T_(c, "error.price_required")
case errors.Is(err, ErrSpecificPriceNotFound):
return i18n.T_(c, "error.price_reduction_not_found")
case errors.Is(err, ErrJSONBody): case errors.Is(err, ErrJSONBody):
return i18n.T_(c, "error.err_json_body") return i18n.T_(c, "error.err_json_body")
@@ -268,6 +298,13 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrUserHasNoSuchCart),
errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrProductOrItsVariationDoesNotExist),
errors.Is(err, ErrAmountMustBePositive),
errors.Is(err, ErrAmountMustBeReasonable),
errors.Is(err, ErrEmptyCart),
errors.Is(err, ErrUserHasNoSuchOrder),
errors.Is(err, ErrInvalidReductionType),
errors.Is(err, ErrPercentageRequired),
errors.Is(err, ErrPriceRequired),
errors.Is(err, ErrAccessDenied), errors.Is(err, ErrAccessDenied),
errors.Is(err, ErrFolderDoesNotExist), errors.Is(err, ErrFolderDoesNotExist),
errors.Is(err, ErrFileDoesNotExist), errors.Is(err, ErrFileDoesNotExist),
@@ -279,6 +316,8 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidCountryID), errors.Is(err, ErrInvalidCountryID),
errors.Is(err, ErrInvalidAddressJSON): errors.Is(err, ErrInvalidAddressJSON):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrSpecificPriceNotFound):
return fiber.StatusNotFound
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict
case errors.Is(err, ErrAIResponseFail), case errors.Is(err, ErrAIResponseFail),

View File

@@ -18,3 +18,7 @@ type EmailAdminNotificationData struct {
type EmailPasswordResetData struct { type EmailPasswordResetData struct {
ResetURL string ResetURL string
} }
type EmailNewOrderPlacedData struct {
UserID uint
}

100
app/view/product.go Normal file
View File

@@ -0,0 +1,100 @@
package view
import (
"encoding/json"
"time"
)
type ProductAttribute struct {
IDProductAttribute uint `gorm:"column:id_product_attribute" json:"id_product_attribute"`
Reference string `gorm:"column:reference" json:"reference"`
BasePrice float64 `gorm:"column:base_price" json:"base_price"`
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
Quantity int64 `gorm:"column:quantity" json:"quantity"`
Attributes json.RawMessage `gorm:"column:attributes" json:"attributes"`
}
type Price struct {
Base float64 `json:"base"`
FinalTaxExcl float64 `json:"final_tax_excl"`
FinalTaxIncl float64 `json:"final_tax_incl"`
TaxRate float64 `json:"tax_rate"`
Priority int `json:"priority"` // or string
}
type Variant struct {
ID uint `json:"id_product_attribute"`
Reference string `json:"reference"`
BasePrice float64 `json:"base_price"`
FinalExcl float64 `json:"final_tax_excl"`
FinalIncl float64 `json:"final_tax_incl"`
Stock int `json:"stock"`
}
type ProductFull struct {
Product Product `json:"product"`
Price Price `json:"price"`
Variants []ProductAttribute `json:"variants,omitempty"`
}
type Product struct {
ID uint `gorm:"column:id" json:"id"`
Reference string `gorm:"column:reference" json:"reference"`
SupplierReference string `gorm:"column:supplier_reference" json:"supplier_reference,omitempty"`
EAN13 string `gorm:"column:ean13" json:"ean13,omitempty"`
UPC string `gorm:"column:upc" json:"upc,omitempty"`
ISBN string `gorm:"column:isbn" json:"isbn,omitempty"`
// Basic Price (from product table)
BasePrice float64 `gorm:"column:base_price" json:"base_price"`
WholesalePrice float64 `gorm:"column:wholesale_price" json:"wholesale_price,omitempty"`
Unity string `gorm:"column:unity" json:"unity,omitempty"`
UnitPriceRatio float64 `gorm:"column:unit_price_ratio" json:"unit_price_ratio,omitempty"`
// Stock & Availability
Quantity int `gorm:"column:quantity" json:"quantity"`
MinimalQuantity int `gorm:"column:minimal_quantity" json:"minimal_quantity"`
AvailableForOrder bool `gorm:"column:available_for_order" json:"available_for_order"`
AvailableDate string `gorm:"column:available_date" json:"available_date,omitempty"`
OutOfStockBehavior int `gorm:"column:out_of_stock_behavior" json:"out_of_stock_behavior"`
// Flags
OnSale bool `gorm:"column:on_sale" json:"on_sale"`
ShowPrice bool `gorm:"column:show_price" json:"show_price"`
Condition string `gorm:"column:condition" json:"condition"`
IsVirtual bool `gorm:"column:is_virtual" json:"is_virtual"`
// Physical
Weight float64 `gorm:"column:weight" json:"weight"`
Width float64 `gorm:"column:width" json:"width"`
Height float64 `gorm:"column:height" json:"height"`
Depth float64 `gorm:"column:depth" json:"depth"`
AdditionalShippingCost float64 `gorm:"column:additional_shipping_cost" json:"additional_shipping_cost,omitempty"`
// Delivery
DeliveryDays int `gorm:"column:delivery_days" json:"delivery_days,omitempty"`
// Status
Active bool `gorm:"column:active" json:"active"`
Visibility string `gorm:"column:visibility" json:"visibility"`
Indexed bool `gorm:"column:indexed" json:"indexed"`
// Timestamps
DateAdd time.Time `gorm:"column:date_add" json:"date_add"`
DateUpd time.Time `gorm:"column:date_upd" json:"date_upd"`
// Language fields
Name string `gorm:"column:name" json:"name"`
Description string `gorm:"column:description" json:"description"`
DescriptionShort string `gorm:"column:description_short" json:"description_short"`
// Relations
Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"`
Category string `gorm:"column:category" json:"category"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
IsOEM bool `gorm:"column:is_oem" json:"is_oem"`
IsNew bool `gorm:"column:is_new" json:"is_new"`
}

View File

@@ -1,7 +1,7 @@
info: info:
name: Change Locales name: Change Locales
type: http type: http
seq: 3 seq: 5
http: http:
method: POST method: POST

View File

@@ -1,7 +1,7 @@
info: info:
name: Delete Index - MeiliSearch name: Delete Index - MeiliSearch
type: http type: http
seq: 5 seq: 7
http: http:
method: DELETE method: DELETE

View File

@@ -1,7 +1,7 @@
info: info:
name: Search Index Settings name: Search Index Settings
type: http type: http
seq: 4 seq: 6
http: http:
method: POST method: POST

View File

@@ -1,7 +1,7 @@
info: info:
name: Search Items name: Search Items
type: http type: http
seq: 2 seq: 4
http: http:
method: POST method: POST

View File

@@ -1,7 +1,7 @@
info: info:
name: auth name: auth
type: folder type: folder
seq: 6 seq: 2
request: request:
auth: inherit auth: inherit

View File

@@ -11,7 +11,7 @@ http:
data: |- data: |-
{ {
"b2b_id_currency" : 1, "b2b_id_currency" : 1,
"conversion_rate": 4.2 "conversion_rate": 3
} }
auth: inherit auth: inherit

View File

@@ -11,7 +11,7 @@ http:
runtime: runtime:
variables: variables:
- name: id - name: id
value: "1" value: "2"
settings: settings:
encodeUrl: true encodeUrl: true

View File

@@ -1,7 +1,7 @@
info: info:
name: currency name: currency
type: folder type: folder
seq: 8 seq: 9
request: request:
auth: inherit auth: inherit

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: "{{bas_url}}/restricted/customer?id=1" url: "{{bas_url}}/restricted/customer?id=2"
params: params:
- name: id - name: id
value: "1" value: "2"
type: query type: query
auth: inherit auth: inherit

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: "{{bas_url}}/restricted/customer/list?search=" url: "{{bas_url}}/restricted/customer/list?search=marek"
params: params:
- name: search - name: search
value: "" value: marek
type: query type: query
auth: inherit auth: inherit

View File

@@ -0,0 +1,22 @@
info:
name: Set is_no_vat
type: http
seq: 4
http:
method: PATCH
url: "{{bas_url}}/restricted/customer/no-vat"
body:
type: json
data: |-
{
"customer_id":1,
"is_no_vat": false
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,7 +1,7 @@
info: info:
name: customer name: customer
type: folder type: folder
seq: 9 seq: 10
request: request:
auth: inherit auth: inherit

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{bas_url}}/restricted/product/200/1/5" url: "{{bas_url}}/restricted/product/51/1/7"
auth: inherit auth: inherit
settings: settings:

View File

@@ -0,0 +1,22 @@
info:
name: Product Variants List
type: http
seq: 3
http:
method: GET
url: "{{bas_url}}/restricted/product/list-variants/{{product_id}}"
body:
type: json
data: ""
runtime:
variables:
- name: product_id
value: "51"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{bas_url}}/restricted/product/list?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62" url: "{{bas_url}}/restricted/product/list?p=1&elems=30&reference=~NC100&is_new_eq=0&is_favorite_eq=false&is_oem_eq=FALSE"
params: params:
- name: p - name: p
value: "1" value: "1"
@@ -16,11 +16,22 @@ http:
- name: sort - name: sort
value: product_id,asc value: product_id,asc
type: query type: query
disabled: true
- name: category_id_in - name: category_id_in
value: "243" value: "23"
type: query type: query
disabled: true
- name: reference - name: reference
value: ~62 value: ~NC100
type: query
- name: is_new_eq
value: "0"
type: query
- name: is_favorite_eq
value: "false"
type: query
- name: is_oem_eq
value: "FALSE"
type: query type: query
body: body:
type: json type: json

View File

@@ -1,7 +1,7 @@
info: info:
name: product name: product
type: folder type: folder
seq: 7 seq: 8
request: request:
auth: inherit auth: inherit

View File

@@ -0,0 +1,15 @@
info:
name: Routes
type: http
seq: 1
http:
method: GET
url: ""
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,7 +1,7 @@
info: info:
name: list name: routes
type: folder type: folder
seq: 3 seq: 10
request: request:
auth: inherit auth: inherit

View File

@@ -0,0 +1,20 @@
info:
name: Activate
type: http
seq: 5
http:
method: PATCH
url: "{{bas_url}}/restricted/specific-price/{{id}}/activate"
auth: inherit
runtime:
variables:
- name: id
value: "1"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,27 @@
info:
name: Create
type: http
seq: 3
http:
method: POST
url: "{{bas_url}}/restricted/specific-price"
body:
type: json
data: |-
{
"name": "Summer Sale 3",
"scope": "shop",
"reduction_type": "amount",
"price": 69,
"from_quantity": 1,
"is_active": true,
"currency_id": 2
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,20 @@
info:
name: Deactivate
type: http
seq: 6
http:
method: PATCH
url: "{{bas_url}}/restricted/specific-price/{{id}}/deactivate"
auth: inherit
runtime:
variables:
- name: id
value: "1"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,20 @@
info:
name: Delete
type: http
seq: 7
http:
method: DELETE
url: "{{bas_url}}/restricted/price-reductions/{{id}}"
auth: inherit
runtime:
variables:
- name: id
value: "1"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,20 @@
info:
name: Get
type: http
seq: 2
http:
method: GET
url: "{{bas_url}}/restricted/specific-price/{{id}}"
auth: inherit
runtime:
variables:
- name: id
value: "1"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: List
type: http
seq: 1
http:
method: GET
url: "{{bas_url}}/restricted/specific-price"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,38 @@
info:
name: Update
type: http
seq: 4
http:
method: PUT
url: "{{bas_url}}/restricted/specific-price/{{id}}"
body:
type: json
data: |-
{
"name": "Summer Sale Updated",
"reduction_type": "amount",
"percentage_reduction": 50.0,
"price": 69,
"currency_id": 1,
"scope": "shop",
"is_active": true,
"from_quantity":1
// "product_ids": [51,53],
// "category_ids": [1],
// "product_attribute_ids": [1114],
// "country_ids": [1],
// "customer_ids": [2,1]
}
auth: inherit
runtime:
variables:
- name: id
value: "3"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,73 @@
info:
name: specific_price
type: folder
seq: 3
docs:
content: |
# Specific Price API
Endpoints for managing specific price rules (price reductions).
## Scopes
Specific prices can be **global** or **scoped**:
- **Global**: If all scope arrays (`product_ids`, `category_ids`, `product_attribute_ids`, `country_ids`, `customer_ids`) are empty, the price reduction applies to everything.
- **Scoped**: If ANY scope array has values, the price reduction applies only when ANY condition matches (UNION logic).
### Scope Fields
| Field | Type | Description |
|-------|------|-------------|
| `product_ids` | uint[] | Specific products |
| `category_ids` | uint[] | Products in categories |
| `product_attribute_ids` | uint[] | Product variants (e.g., size, color) |
| `country_ids` | uint[] | Customers in countries |
| `customer_ids` | uint[] | Specific customers |
### Examples
**Global** (applies to all products):
```json
{
"name": "Global Sale",
"reduction_type": "percentage",
"percentage_reduction": 10,
"from_quantity": 1
}
```
**Scoped to specific products**:
```json
{
"name": "Product Sale",
"reduction_type": "percentage",
"percentage_reduction": 20,
"product_ids": [1, 2, 3]
}
```
**Scoped to category + country**:
```json
{
"name": "Category Country Sale",
"reduction_type": "amount",
"price": 9.99,
"category_ids": [5],
"country_ids": [1]
}
```
## Reduction Types
- `percentage`: Requires `percentage_reduction` (e.g., 10.5 = 10.5% off)
- `amount`: Requires `price` (fixed price after reduction)
## Validation
- `reduction_type` is required and must be "percentage" or "amount"
- If `reduction_type` is "percentage", then `percentage_reduction` is required
- If `reduction_type` is "amount", then `price` is required
type: text/markdown

View File

@@ -1,7 +1,7 @@
info: info:
name: addresses name: addresses
type: folder type: folder
seq: 10 seq: 9
request: request:
auth: inherit auth: inherit

View File

@@ -5,15 +5,14 @@ info:
http: http:
method: POST method: POST
url: http://localhost:3000/api/v1/public/auth/update-choice?lang_id=0&country_id=1 url: http://localhost:3000/api/v1/public/auth/update-choice?lang_id=1&country_id=1
params: params:
- name: lang_id - name: lang_id
value: "0" value: "1"
type: query type: query
- name: country_id - name: country_id
value: "1" value: "1"
type: query type: query
auth: inherit
settings: settings:
encodeUrl: true encodeUrl: true

View File

@@ -5,7 +5,11 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/carts/add-new-cart url: http://localhost:3000/api/v1/restricted/carts/add-new-cart?name=carttt
params:
- name: name
value: carttt
type: query
auth: inherit auth: inherit
settings: settings:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&amount=1 url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&amount=1&set_amount=false
params: params:
- name: cart_id - name: cart_id
value: "1" value: "1"
@@ -16,6 +16,9 @@ http:
- name: amount - name: amount
value: "1" value: "1"
type: query type: query
- name: set_amount
value: "false"
type: query
auth: inherit auth: inherit
settings: settings:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=1 url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=2&set_amount=true
params: params:
- name: cart_id - name: cart_id
value: "1" value: "1"
@@ -17,7 +17,10 @@ http:
value: "1115" value: "1115"
type: query type: query
- name: amount - name: amount
value: "1" value: "2"
type: query
- name: set_amount
value: "true"
type: query type: query
auth: inherit auth: inherit

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=3 url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=1
params: params:
- name: cart_id - name: cart_id
value: "3" value: "1"
type: query type: query
auth: inherit auth: inherit

View File

@@ -1,24 +0,0 @@
info:
name: list-products
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10&target_user_id=2
params:
- name: p
value: "1"
type: query
- name: elems
value: "10"
type: query
- name: target_user_id
value: "2"
type: query
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,21 +0,0 @@
info:
name: list-users
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/list/list-users?p=1&elems=10
params:
- name: p
value: "1"
type: query
- name: elems
value: "10"
type: query
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=10&category_id=13 url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=2&category_id=13
params: params:
- name: root_category_id - name: root_category_id
value: "10" value: "2"
type: query type: query
- name: category_id - name: category_id
value: "13" value: "13"

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=10 url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=2
params: params:
- name: root_category_id - name: root_category_id
value: "10" value: "2"
type: query type: query
auth: inherit auth: inherit

View File

@@ -0,0 +1,33 @@
info:
name: change-order-address
type: http
seq: 3
http:
method: GET
url: http://localhost:3000/api/v1/restricted/orders/change-order-address?order_id=1&country_id=1
params:
- name: order_id
value: "1"
type: query
- name: country_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "śląskie",
"street": "Długa",
"building_no": "5",
"recipient": "Adam Adamowicz"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: change-order-status
type: http
seq: 4
http:
method: GET
url: http://localhost:3000/api/v1/restricted/orders/change-order-status?order_id=1&status=PAID
params:
- name: order_id
value: "1"
type: query
- name: status
value: PAID
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: orders
type: folder
seq: 10
request:
auth: inherit

View File

@@ -0,0 +1,31 @@
info:
name: list
type: http
seq: 2
http:
method: GET
url: http://localhost:3000/api/v1/restricted/orders/list?p=1&elems=30&sort=product_id,asc&user_id=2&name=~sdj
params:
- name: p
value: "1"
type: query
- name: elems
value: "30"
type: query
- name: sort
value: product_id,asc
type: query
- name: user_id
value: "2"
type: query
- name: name
value: ~sdj
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

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