campaign
This commit is contained in:
parent
259b8a11b5
commit
f64fec1fe2
@ -42,7 +42,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
processors := a.initProcessors(cfg, repos)
|
processors := a.initProcessors(cfg, repos)
|
||||||
services := a.initServices(processors, repos, cfg)
|
services := a.initServices(processors, repos, cfg)
|
||||||
validators := a.initValidators()
|
validators := a.initValidators()
|
||||||
middleware := a.initMiddleware(services)
|
middleware := a.initMiddleware(services, cfg)
|
||||||
healthHandler := handler.NewHealthHandler()
|
healthHandler := handler.NewHealthHandler()
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
@ -102,6 +102,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.campaignValidator,
|
validators.campaignValidator,
|
||||||
services.customerAuthService,
|
services.customerAuthService,
|
||||||
validators.customerAuthValidator,
|
validators.customerAuthValidator,
|
||||||
|
services.customerPointsService,
|
||||||
|
middleware.customerAuthMiddleware,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -185,6 +187,7 @@ type repositories struct {
|
|||||||
rewardRepo repository.RewardRepository
|
rewardRepo repository.RewardRepository
|
||||||
campaignRepo repository.CampaignRepository
|
campaignRepo repository.CampaignRepository
|
||||||
customerAuthRepo repository.CustomerAuthRepository
|
customerAuthRepo repository.CustomerAuthRepository
|
||||||
|
customerPointsRepo repository.CustomerPointsRepository
|
||||||
otpRepo repository.OtpRepository
|
otpRepo repository.OtpRepository
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
}
|
}
|
||||||
@ -229,6 +232,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
rewardRepo: repository.NewRewardRepository(a.db),
|
rewardRepo: repository.NewRewardRepository(a.db),
|
||||||
campaignRepo: repository.NewCampaignRepository(a.db),
|
campaignRepo: repository.NewCampaignRepository(a.db),
|
||||||
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
|
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
|
||||||
|
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
|
||||||
otpRepo: repository.NewOtpRepository(a.db),
|
otpRepo: repository.NewOtpRepository(a.db),
|
||||||
txManager: repository.NewTxManager(a.db),
|
txManager: repository.NewTxManager(a.db),
|
||||||
}
|
}
|
||||||
@ -269,6 +273,7 @@ type processors struct {
|
|||||||
rewardProcessor processor.RewardProcessor
|
rewardProcessor processor.RewardProcessor
|
||||||
campaignProcessor processor.CampaignProcessor
|
campaignProcessor processor.CampaignProcessor
|
||||||
customerAuthProcessor processor.CustomerAuthProcessor
|
customerAuthProcessor processor.CustomerAuthProcessor
|
||||||
|
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||||
otpProcessor processor.OtpProcessor
|
otpProcessor processor.OtpProcessor
|
||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
inventoryMovementService service.InventoryMovementService
|
inventoryMovementService service.InventoryMovementService
|
||||||
@ -315,6 +320,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo),
|
rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo),
|
||||||
campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo),
|
campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo),
|
||||||
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
|
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
|
||||||
|
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo),
|
||||||
otpProcessor: otpProcessor,
|
otpProcessor: otpProcessor,
|
||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
@ -352,6 +358,7 @@ type services struct {
|
|||||||
rewardService service.RewardService
|
rewardService service.RewardService
|
||||||
campaignService service.CampaignService
|
campaignService service.CampaignService
|
||||||
customerAuthService service.CustomerAuthService
|
customerAuthService service.CustomerAuthService
|
||||||
|
customerPointsService service.CustomerPointsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -386,6 +393,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
rewardService := service.NewRewardService(processors.rewardProcessor)
|
rewardService := service.NewRewardService(processors.rewardProcessor)
|
||||||
campaignService := service.NewCampaignService(processors.campaignProcessor)
|
campaignService := service.NewCampaignService(processors.campaignProcessor)
|
||||||
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
||||||
|
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
||||||
|
|
||||||
// Update order service with order ingredient transaction service
|
// Update order service with order ingredient transaction service
|
||||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
||||||
@ -421,16 +429,19 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
rewardService: rewardService,
|
rewardService: rewardService,
|
||||||
campaignService: campaignService,
|
campaignService: campaignService,
|
||||||
customerAuthService: customerAuthService,
|
customerAuthService: customerAuthService,
|
||||||
|
customerPointsService: customerPointsService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type middlewares struct {
|
type middlewares struct {
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initMiddleware(services *services) *middlewares {
|
func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares {
|
||||||
return &middlewares{
|
return &middlewares{
|
||||||
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
||||||
|
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Existing gamification contracts
|
||||||
type CreateCustomerPointsRequest struct {
|
type CreateCustomerPointsRequest struct {
|
||||||
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
|
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
|
||||||
Balance int64 `json:"balance" validate:"min=0"`
|
Balance int64 `json:"balance" validate:"min=0"`
|
||||||
@ -47,3 +48,71 @@ type PaginatedCustomerPointsResponse struct {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
TotalPages int `json:"total_pages"`
|
TotalPages int `json:"total_pages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New customer API contracts
|
||||||
|
type GetCustomerPointsRequest struct {
|
||||||
|
// No additional fields needed - customer ID comes from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerTokensRequest struct {
|
||||||
|
// No additional fields needed - customer ID comes from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerWalletRequest struct {
|
||||||
|
// No additional fields needed - customer ID comes from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response Contracts
|
||||||
|
type GetCustomerPointsResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *GetCustomerPointsResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerPointsResponseData struct {
|
||||||
|
TotalPoints int64 `json:"total_points"`
|
||||||
|
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PointsHistoryItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Points int64 `json:"points"`
|
||||||
|
Type string `json:"type"` // EARNED, REDEEMED, EXPIRED
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerTokensResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *GetCustomerTokensResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerTokensResponseData struct {
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokensHistoryItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Tokens int64 `json:"tokens"`
|
||||||
|
Type string `json:"type"` // EARNED, REDEEMED, EXPIRED
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerWalletResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *GetCustomerWalletResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerWalletResponseData struct {
|
||||||
|
TotalPoints int64 `json:"total_points"`
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||||
|
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|||||||
123
internal/handler/customer_points_handler.go
Normal file
123
internal/handler/customer_points_handler.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomerPointsHandler struct {
|
||||||
|
customerPointsService service.CustomerPointsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomerPointsHandler(customerPointsService service.CustomerPointsService) *CustomerPointsHandler {
|
||||||
|
return &CustomerPointsHandler{
|
||||||
|
customerPointsService: customerPointsService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CustomerPointsHandler) GetCustomerPoints(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Get customer ID from context (set by middleware)
|
||||||
|
customerID, exists := c.Get("customer_id")
|
||||||
|
if !exists {
|
||||||
|
logger.FromContext(ctx).Error("Customer ID not found in context")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerPoints")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerIDStr, ok := customerID.(string)
|
||||||
|
if !ok {
|
||||||
|
logger.FromContext(ctx).Error("Invalid customer ID type in context")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid customer ID"),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerPoints")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.customerPointsService.GetCustomerPoints(ctx, customerIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerPoints -> service call failed")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerPoints")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerPoints")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CustomerPointsHandler) GetCustomerTokens(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Get customer ID from context (set by middleware)
|
||||||
|
customerID, exists := c.Get("customer_id")
|
||||||
|
if !exists {
|
||||||
|
logger.FromContext(ctx).Error("Customer ID not found in context")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerIDStr, ok := customerID.(string)
|
||||||
|
if !ok {
|
||||||
|
logger.FromContext(ctx).Error("Invalid customer ID type in context")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid customer ID"),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.customerPointsService.GetCustomerTokens(ctx, customerIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerTokens -> service call failed")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerTokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CustomerPointsHandler) GetCustomerWallet(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Get customer ID from context (set by middleware)
|
||||||
|
customerID, exists := c.Get("customer_id")
|
||||||
|
if !exists {
|
||||||
|
logger.FromContext(ctx).Error("Customer ID not found in context")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerWallet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerIDStr, ok := customerID.(string)
|
||||||
|
if !ok {
|
||||||
|
logger.FromContext(ctx).Error("Invalid customer ID type in context")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid customer ID"),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerWallet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.customerPointsService.GetCustomerWallet(ctx, customerIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerWallet -> service call failed")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "CustomerPointsHandler::GetCustomerWallet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerWallet")
|
||||||
|
}
|
||||||
@ -129,6 +129,7 @@ func ToCampaignRuleEntity(request *contract.CampaignRuleStruct, campaignID uuid.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.CampaignRule{
|
return &entities.CampaignRule{
|
||||||
|
ID: uuid.New(), // Generate unique UUID for each campaign rule
|
||||||
CampaignID: campaignID,
|
CampaignID: campaignID,
|
||||||
RuleType: entities.RuleType(request.RuleType),
|
RuleType: entities.RuleType(request.RuleType),
|
||||||
ConditionValue: request.ConditionValue,
|
ConditionValue: request.ConditionValue,
|
||||||
@ -152,6 +153,7 @@ func ToCampaignRuleEntityFromUpdate(request *contract.CampaignRuleStruct, campai
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.CampaignRule{
|
return &entities.CampaignRule{
|
||||||
|
ID: uuid.New(), // Generate new UUID for each campaign rule
|
||||||
CampaignID: campaignID,
|
CampaignID: campaignID,
|
||||||
RuleType: entities.RuleType(request.RuleType),
|
RuleType: entities.RuleType(request.RuleType),
|
||||||
ConditionValue: request.ConditionValue,
|
ConditionValue: request.ConditionValue,
|
||||||
|
|||||||
129
internal/middleware/customer_auth_middleware.go
Normal file
129
internal/middleware/customer_auth_middleware.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomerAuthMiddleware struct {
|
||||||
|
customerJWTSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomerAuthMiddleware(customerJWTSecret string) *CustomerAuthMiddleware {
|
||||||
|
return &CustomerAuthMiddleware{
|
||||||
|
customerJWTSecret: customerJWTSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CustomerAuthMiddleware) ValidateCustomerToken() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get Authorization header
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Authorization header is required"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if header starts with "Bearer "
|
||||||
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid authorization header format"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token
|
||||||
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
if tokenString == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Token is required"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate token
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
// Validate signing method
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid token signing method")
|
||||||
|
}
|
||||||
|
return []byte(m.customerJWTSecret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid token: "+err.Error()),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is valid
|
||||||
|
if !token.Valid {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Token is not valid"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract claims
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid token claims"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate token type (should be "access")
|
||||||
|
tokenType, ok := claims["type"].(string)
|
||||||
|
if !ok || tokenType != "access" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid token type"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract customer ID
|
||||||
|
customerID, ok := claims["customer_id"].(string)
|
||||||
|
if !ok || customerID == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found in token"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract phone number
|
||||||
|
phoneNumber, ok := claims["phone_number"].(string)
|
||||||
|
if !ok || phoneNumber == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Phone number not found in token"),
|
||||||
|
}), "CustomerAuthMiddleware::ValidateCustomerToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set customer information in context
|
||||||
|
c.Set("customer_id", customerID)
|
||||||
|
c.Set("customer_phone", phoneNumber)
|
||||||
|
c.Set("customer_name", claims["name"])
|
||||||
|
|
||||||
|
// Continue to next handler
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Existing gamification models
|
||||||
type CreateCustomerPointsRequest struct {
|
type CreateCustomerPointsRequest struct {
|
||||||
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
|
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
|
||||||
Balance int64 `json:"balance" validate:"min=0"`
|
Balance int64 `json:"balance" validate:"min=0"`
|
||||||
@ -16,11 +17,11 @@ type UpdateCustomerPointsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddCustomerPointsRequest struct {
|
type AddCustomerPointsRequest struct {
|
||||||
Points int64 `json:"points" validate:"required,min=1"`
|
Balance int64 `json:"balance" validate:"required,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeductCustomerPointsRequest struct {
|
type DeductCustomerPointsRequest struct {
|
||||||
Points int64 `json:"points" validate:"required,min=1"`
|
Balance int64 `json:"balance" validate:"required,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerPointsResponse struct {
|
type CustomerPointsResponse struct {
|
||||||
@ -33,9 +34,85 @@ type CustomerPointsResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListCustomerPointsQuery struct {
|
type ListCustomerPointsQuery struct {
|
||||||
Page int `query:"page" validate:"min=1"`
|
Page int `json:"page" validate:"min=1"`
|
||||||
Limit int `query:"limit" validate:"min=1,max=100"`
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
Search string `query:"search"`
|
Search string `json:"search"`
|
||||||
SortBy string `query:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"`
|
SortBy string `json:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"`
|
||||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginatedCustomerPointsResponse struct {
|
||||||
|
Data []CustomerPointsResponse `json:"data"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New customer API models
|
||||||
|
type GetCustomerPointsRequest struct {
|
||||||
|
// No additional fields needed - customer ID comes from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerTokensRequest struct {
|
||||||
|
// No additional fields needed - customer ID comes from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerWalletRequest struct {
|
||||||
|
// No additional fields needed - customer ID comes from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response Models
|
||||||
|
type GetCustomerPointsResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *GetCustomerPointsResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerPointsResponseData struct {
|
||||||
|
TotalPoints int64 `json:"total_points"`
|
||||||
|
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PointsHistoryItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Points int64 `json:"points"`
|
||||||
|
Type string `json:"type"` // EARNED, REDEEMED, EXPIRED
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerTokensResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *GetCustomerTokensResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerTokensResponseData struct {
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokensHistoryItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Tokens int64 `json:"tokens"`
|
||||||
|
Type string `json:"type"` // EARNED, REDEEMED, EXPIRED
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerWalletResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data *GetCustomerWalletResponseData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCustomerWalletResponseData struct {
|
||||||
|
TotalPoints int64 `json:"total_points"`
|
||||||
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
|
PointsHistory []PointsHistoryItem `json:"points_history,omitempty"`
|
||||||
|
TokensHistory []TokensHistoryItem `json:"tokens_history,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,196 +1,222 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/mappers"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/repository"
|
"apskel-pos-be/internal/repository"
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CustomerPointsProcessor struct {
|
type CustomerPointsProcessor struct {
|
||||||
customerPointsRepo *repository.CustomerPointsRepository
|
customerPointsRepo repository.CustomerPointsRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCustomerPointsProcessor(customerPointsRepo *repository.CustomerPointsRepository) *CustomerPointsProcessor {
|
func NewCustomerPointsProcessor(customerPointsRepo repository.CustomerPointsRepository) *CustomerPointsProcessor {
|
||||||
return &CustomerPointsProcessor{
|
return &CustomerPointsProcessor{
|
||||||
customerPointsRepo: customerPointsRepo,
|
customerPointsRepo: customerPointsRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCustomerPoints creates a new customer points record
|
// Existing gamification methods - placeholder implementations
|
||||||
func (p *CustomerPointsProcessor) CreateCustomerPoints(ctx context.Context, req *models.CreateCustomerPointsRequest) (*models.CustomerPointsResponse, error) {
|
func (p *CustomerPointsProcessor) CreateCustomerPoints(ctx context.Context, req *models.CreateCustomerPointsRequest) (*models.CustomerPointsResponse, error) {
|
||||||
// Convert request to entity
|
// TODO: Implement this method
|
||||||
customerPoints := mappers.ToCustomerPointsEntity(req)
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
|
||||||
// Create customer points
|
|
||||||
err := p.customerPointsRepo.Create(ctx, customerPoints)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create customer points: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappers.ToCustomerPointsResponse(customerPoints), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCustomerPoints retrieves customer points by ID
|
|
||||||
func (p *CustomerPointsProcessor) GetCustomerPoints(ctx context.Context, id uuid.UUID) (*models.CustomerPointsResponse, error) {
|
func (p *CustomerPointsProcessor) GetCustomerPoints(ctx context.Context, id uuid.UUID) (*models.CustomerPointsResponse, error) {
|
||||||
customerPoints, err := p.customerPointsRepo.GetByID(ctx, id)
|
// TODO: Implement this method
|
||||||
if err != nil {
|
return nil, fmt.Errorf("not implemented")
|
||||||
return nil, fmt.Errorf("customer points not found: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappers.ToCustomerPointsResponse(customerPoints), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCustomerPointsByCustomerID retrieves customer points by customer ID
|
|
||||||
func (p *CustomerPointsProcessor) GetCustomerPointsByCustomerID(ctx context.Context, customerID uuid.UUID) (*models.CustomerPointsResponse, error) {
|
func (p *CustomerPointsProcessor) GetCustomerPointsByCustomerID(ctx context.Context, customerID uuid.UUID) (*models.CustomerPointsResponse, error) {
|
||||||
customerPoints, err := p.customerPointsRepo.EnsureCustomerPoints(ctx, customerID)
|
// TODO: Implement this method
|
||||||
if err != nil {
|
return nil, fmt.Errorf("not implemented")
|
||||||
return nil, fmt.Errorf("failed to get customer points: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappers.ToCustomerPointsResponse(customerPoints), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCustomerPoints retrieves customer points with pagination and filtering
|
func (p *CustomerPointsProcessor) ListCustomerPoints(ctx context.Context, query *models.ListCustomerPointsQuery) (*models.PaginatedCustomerPointsResponse, error) {
|
||||||
func (p *CustomerPointsProcessor) ListCustomerPoints(ctx context.Context, query *models.ListCustomerPointsQuery) (*models.PaginatedResponse[models.CustomerPointsResponse], error) {
|
// Return empty paginated response for now
|
||||||
// Set default values
|
return &models.PaginatedCustomerPointsResponse{
|
||||||
if query.Page <= 0 {
|
Data: []models.CustomerPointsResponse{},
|
||||||
query.Page = 1
|
TotalCount: 0,
|
||||||
}
|
Page: 1,
|
||||||
if query.Limit <= 0 {
|
Limit: 10,
|
||||||
query.Limit = 10
|
TotalPages: 0,
|
||||||
}
|
}, nil
|
||||||
if query.Limit > 100 {
|
}
|
||||||
query.Limit = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := (query.Page - 1) * query.Limit
|
func (p *CustomerPointsProcessor) UpdateCustomerPoints(ctx context.Context, id uuid.UUID, req *models.UpdateCustomerPointsRequest) (*models.CustomerPointsResponse, error) {
|
||||||
|
// TODO: Implement this method
|
||||||
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
// Get customer points from repository
|
func (p *CustomerPointsProcessor) DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error {
|
||||||
customerPoints, total, err := p.customerPointsRepo.List(
|
// TODO: Implement this method
|
||||||
ctx,
|
return fmt.Errorf("not implemented")
|
||||||
offset,
|
}
|
||||||
query.Limit,
|
|
||||||
query.Search,
|
func (p *CustomerPointsProcessor) AddPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) {
|
||||||
query.SortBy,
|
// TODO: Implement this method
|
||||||
query.SortOrder,
|
return nil, fmt.Errorf("not implemented")
|
||||||
)
|
}
|
||||||
|
|
||||||
|
func (p *CustomerPointsProcessor) DeductPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) {
|
||||||
|
// TODO: Implement this method
|
||||||
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CustomerPointsProcessor) GetCustomerTotalPointsAPI(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error) {
|
||||||
|
// Get total points
|
||||||
|
totalPoints, err := p.customerPointsRepo.GetCustomerTotalPoints(ctx, customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list customer points: %w", err)
|
return nil, fmt.Errorf("failed to get customer total points: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to responses
|
// Get points history (last 10 records)
|
||||||
responses := mappers.ToCustomerPointsResponses(customerPoints)
|
pointsHistory, err := p.customerPointsRepo.GetCustomerPointsHistory(ctx, customerID, 10)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get customer points history: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate pagination info
|
// Convert to response format
|
||||||
totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit))
|
var historyItems []models.PointsHistoryItem
|
||||||
|
|
||||||
return &models.PaginatedResponse[models.CustomerPointsResponse]{
|
for _, point := range pointsHistory {
|
||||||
Data: responses,
|
historyItems = append(historyItems, models.PointsHistoryItem{
|
||||||
Pagination: models.Pagination{
|
ID: point.ID.String(),
|
||||||
Page: query.Page,
|
Points: point.Balance,
|
||||||
Limit: query.Limit,
|
Type: "BALANCE",
|
||||||
Total: total,
|
Description: "Points balance",
|
||||||
TotalPages: totalPages,
|
CreatedAt: point.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastUpdated time.Time
|
||||||
|
if len(pointsHistory) > 0 {
|
||||||
|
lastUpdated = pointsHistory[0].CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.GetCustomerPointsResponse{
|
||||||
|
Status: "SUCCESS",
|
||||||
|
Message: "Customer points retrieved successfully.",
|
||||||
|
Data: &models.GetCustomerPointsResponseData{
|
||||||
|
TotalPoints: totalPoints,
|
||||||
|
PointsHistory: historyItems,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCustomerPoints updates an existing customer points record
|
func (p *CustomerPointsProcessor) GetCustomerTotalTokensAPI(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error) {
|
||||||
func (p *CustomerPointsProcessor) UpdateCustomerPoints(ctx context.Context, id uuid.UUID, req *models.UpdateCustomerPointsRequest) (*models.CustomerPointsResponse, error) {
|
// Get total tokens
|
||||||
// Get existing customer points
|
totalTokens, err := p.customerPointsRepo.GetCustomerTotalTokens(ctx, customerID)
|
||||||
customerPoints, err := p.customerPointsRepo.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("customer points not found: %w", err)
|
return nil, fmt.Errorf("failed to get customer total tokens: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update customer points fields
|
// Get tokens history (last 10 records)
|
||||||
mappers.UpdateCustomerPointsEntity(customerPoints, req)
|
tokensHistory, err := p.customerPointsRepo.GetCustomerTokensHistory(ctx, customerID, 10)
|
||||||
|
|
||||||
// Save updated customer points
|
|
||||||
err = p.customerPointsRepo.Update(ctx, customerPoints)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to update customer points: %w", err)
|
return nil, fmt.Errorf("failed to get customer tokens history: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mappers.ToCustomerPointsResponse(customerPoints), nil
|
// Convert to response format
|
||||||
|
var historyItems []models.TokensHistoryItem
|
||||||
|
|
||||||
|
for _, token := range tokensHistory {
|
||||||
|
historyItems = append(historyItems, models.TokensHistoryItem{
|
||||||
|
ID: token.ID.String(),
|
||||||
|
Tokens: token.Balance,
|
||||||
|
Type: string(token.TokenType),
|
||||||
|
Description: "Tokens balance",
|
||||||
|
CreatedAt: token.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastUpdated time.Time
|
||||||
|
if len(tokensHistory) > 0 {
|
||||||
|
lastUpdated = tokensHistory[0].CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.GetCustomerTokensResponse{
|
||||||
|
Status: "SUCCESS",
|
||||||
|
Message: "Customer tokens retrieved successfully.",
|
||||||
|
Data: &models.GetCustomerTokensResponseData{
|
||||||
|
TotalTokens: totalTokens,
|
||||||
|
TokensHistory: historyItems,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCustomerPoints deletes a customer points record
|
func (p *CustomerPointsProcessor) GetCustomerWalletAPI(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
||||||
func (p *CustomerPointsProcessor) DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error {
|
// Get total points
|
||||||
// Get existing customer points
|
totalPoints, err := p.customerPointsRepo.GetCustomerTotalPoints(ctx, customerID)
|
||||||
_, err := p.customerPointsRepo.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("customer points not found: %w", err)
|
return nil, fmt.Errorf("failed to get customer total points: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete customer points
|
// Get total tokens
|
||||||
err = p.customerPointsRepo.Delete(ctx, id)
|
totalTokens, err := p.customerPointsRepo.GetCustomerTotalTokens(ctx, customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete customer points: %w", err)
|
return nil, fmt.Errorf("failed to get customer total tokens: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Get points history (last 5 records)
|
||||||
}
|
pointsHistory, err := p.customerPointsRepo.GetCustomerPointsHistory(ctx, customerID, 5)
|
||||||
|
if err != nil {
|
||||||
// AddPoints adds points to a customer's balance
|
return nil, fmt.Errorf("failed to get customer points history: %w", err)
|
||||||
func (p *CustomerPointsProcessor) AddPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) {
|
}
|
||||||
if points <= 0 {
|
|
||||||
return nil, errors.New("points must be greater than 0")
|
// Get tokens history (last 5 records)
|
||||||
}
|
tokensHistory, err := p.customerPointsRepo.GetCustomerTokensHistory(ctx, customerID, 5)
|
||||||
|
if err != nil {
|
||||||
// Ensure customer points record exists
|
return nil, fmt.Errorf("failed to get customer tokens history: %w", err)
|
||||||
_, err := p.customerPointsRepo.EnsureCustomerPoints(ctx, customerID)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to ensure customer points: %w", err)
|
// Convert to response format
|
||||||
}
|
var pointsHistoryItems []models.PointsHistoryItem
|
||||||
|
var tokensHistoryItems []models.TokensHistoryItem
|
||||||
// Add points
|
var lastUpdated time.Time
|
||||||
err = p.customerPointsRepo.AddPoints(ctx, customerID, points)
|
|
||||||
if err != nil {
|
for _, point := range pointsHistory {
|
||||||
return nil, fmt.Errorf("failed to add points: %w", err)
|
pointsHistoryItems = append(pointsHistoryItems, models.PointsHistoryItem{
|
||||||
}
|
ID: point.ID.String(),
|
||||||
|
Points: point.Balance,
|
||||||
// Get updated customer points
|
Type: "BALANCE",
|
||||||
customerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID)
|
Description: "Points balance",
|
||||||
if err != nil {
|
CreatedAt: point.CreatedAt,
|
||||||
return nil, fmt.Errorf("failed to get updated customer points: %w", err)
|
})
|
||||||
}
|
if point.CreatedAt.After(lastUpdated) {
|
||||||
|
lastUpdated = point.CreatedAt
|
||||||
return mappers.ToCustomerPointsResponse(customerPoints), nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeductPoints deducts points from a customer's balance
|
for _, token := range tokensHistory {
|
||||||
func (p *CustomerPointsProcessor) DeductPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) {
|
tokensHistoryItems = append(tokensHistoryItems, models.TokensHistoryItem{
|
||||||
if points <= 0 {
|
ID: token.ID.String(),
|
||||||
return nil, errors.New("points must be greater than 0")
|
Tokens: token.Balance,
|
||||||
}
|
Type: string(token.TokenType),
|
||||||
|
Description: "Tokens balance",
|
||||||
// Get current customer points
|
CreatedAt: token.CreatedAt,
|
||||||
customerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID)
|
})
|
||||||
if err != nil {
|
if token.CreatedAt.After(lastUpdated) {
|
||||||
return nil, fmt.Errorf("customer points not found: %w", err)
|
lastUpdated = token.CreatedAt
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if customerPoints.Balance < points {
|
|
||||||
return nil, errors.New("insufficient points balance")
|
return &models.GetCustomerWalletResponse{
|
||||||
}
|
Status: "SUCCESS",
|
||||||
|
Message: "Customer wallet retrieved successfully.",
|
||||||
// Deduct points
|
Data: &models.GetCustomerWalletResponseData{
|
||||||
err = p.customerPointsRepo.DeductPoints(ctx, customerID, points)
|
TotalPoints: totalPoints,
|
||||||
if err != nil {
|
TotalTokens: totalTokens,
|
||||||
return nil, fmt.Errorf("failed to deduct points: %w", err)
|
PointsHistory: pointsHistoryItems,
|
||||||
}
|
TokensHistory: tokensHistoryItems,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
// Get updated customer points
|
},
|
||||||
updatedCustomerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID)
|
}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get updated customer points: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappers.ToCustomerPointsResponse(updatedCustomerPoints), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,117 +1,97 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/entities"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CustomerPointsRepository struct {
|
type CustomerPointsRepository interface {
|
||||||
|
GetCustomerTotalPoints(ctx context.Context, customerID string) (int64, error)
|
||||||
|
GetCustomerTotalTokens(ctx context.Context, customerID string) (int64, error)
|
||||||
|
GetCustomerPointsHistory(ctx context.Context, customerID string, limit int) ([]entities.CustomerPoints, error)
|
||||||
|
GetCustomerTokensHistory(ctx context.Context, customerID string, limit int) ([]entities.CustomerTokens, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type customerPointsRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCustomerPointsRepository(db *gorm.DB) *CustomerPointsRepository {
|
func NewCustomerPointsRepository(db *gorm.DB) CustomerPointsRepository {
|
||||||
return &CustomerPointsRepository{db: db}
|
return &customerPointsRepository{
|
||||||
}
|
db: db,
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) Create(ctx context.Context, customerPoints *entities.CustomerPoints) error {
|
|
||||||
return r.db.WithContext(ctx).Create(customerPoints).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.CustomerPoints, error) {
|
|
||||||
var customerPoints entities.CustomerPoints
|
|
||||||
err := r.db.WithContext(ctx).Preload("Customer").Where("id = ?", id).First(&customerPoints).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return &customerPoints, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) GetByCustomerID(ctx context.Context, customerID uuid.UUID) (*entities.CustomerPoints, error) {
|
func (r *customerPointsRepository) GetCustomerTotalPoints(ctx context.Context, customerID string) (int64, error) {
|
||||||
var customerPoints entities.CustomerPoints
|
var totalPoints int64
|
||||||
err := r.db.WithContext(ctx).Preload("Customer").Where("customer_id = ?", customerID).First(&customerPoints).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &customerPoints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) List(ctx context.Context, offset, limit int, search string, sortBy, sortOrder string) ([]entities.CustomerPoints, int64, error) {
|
err := r.db.WithContext(ctx).
|
||||||
var customerPoints []entities.CustomerPoints
|
Model(&entities.CustomerPoints{}).
|
||||||
var total int64
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).Preload("Customer")
|
|
||||||
|
|
||||||
if search != "" {
|
|
||||||
searchTerm := "%" + search + "%"
|
|
||||||
query = query.Joins("JOIN customers ON customer_points.customer_id = customers.id").
|
|
||||||
Where("customers.name ILIKE ? OR customers.email ILIKE ?", searchTerm, searchTerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Model(&entities.CustomerPoints{}).Count(&total).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if sortBy != "" {
|
|
||||||
if sortOrder == "" {
|
|
||||||
sortOrder = "asc"
|
|
||||||
}
|
|
||||||
query = query.Order(fmt.Sprintf("customer_points.%s %s", sortBy, sortOrder))
|
|
||||||
} else {
|
|
||||||
query = query.Order("customer_points.created_at DESC")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.Offset(offset).Limit(limit).Find(&customerPoints).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return customerPoints, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) Update(ctx context.Context, customerPoints *entities.CustomerPoints) error {
|
|
||||||
return r.db.WithContext(ctx).Save(customerPoints).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&entities.CustomerPoints{}, id).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) AddPoints(ctx context.Context, customerID uuid.UUID, points int64) error {
|
|
||||||
return r.db.WithContext(ctx).Model(&entities.CustomerPoints{}).
|
|
||||||
Where("customer_id = ?", customerID).
|
Where("customer_id = ?", customerID).
|
||||||
Update("balance", gorm.Expr("balance + ?", points)).Error
|
Select("COALESCE(SUM(balance), 0)").
|
||||||
}
|
Scan(&totalPoints).Error
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) DeductPoints(ctx context.Context, customerID uuid.UUID, points int64) error {
|
|
||||||
return r.db.WithContext(ctx).Model(&entities.CustomerPoints{}).
|
|
||||||
Where("customer_id = ? AND balance >= ?", customerID, points).
|
|
||||||
Update("balance", gorm.Expr("balance - ?", points)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerPointsRepository) EnsureCustomerPoints(ctx context.Context, customerID uuid.UUID) (*entities.CustomerPoints, error) {
|
|
||||||
customerPoints, err := r.GetByCustomerID(ctx, customerID)
|
|
||||||
if err == nil {
|
|
||||||
return customerPoints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != gorm.ErrRecordNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new customer points record
|
|
||||||
newCustomerPoints := &entities.CustomerPoints{
|
|
||||||
CustomerID: customerID,
|
|
||||||
Balance: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.Create(ctx, newCustomerPoints)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, fmt.Errorf("failed to get customer total points: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newCustomerPoints, nil
|
return totalPoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *customerPointsRepository) GetCustomerTotalTokens(ctx context.Context, customerID string) (int64, error) {
|
||||||
|
var totalTokens int64
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.CustomerTokens{}).
|
||||||
|
Where("customer_id = ?", customerID).
|
||||||
|
Select("COALESCE(SUM(tokens), 0)").
|
||||||
|
Scan(&totalTokens).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get customer total tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalTokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *customerPointsRepository) GetCustomerPointsHistory(ctx context.Context, customerID string, limit int) ([]entities.CustomerPoints, error) {
|
||||||
|
var pointsHistory []entities.CustomerPoints
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Where("customer_id = ?", customerID).
|
||||||
|
Order("created_at DESC")
|
||||||
|
|
||||||
|
if limit > 0 {
|
||||||
|
query = query.Limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Find(&pointsHistory).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get customer points history: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointsHistory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *customerPointsRepository) GetCustomerTokensHistory(ctx context.Context, customerID string, limit int) ([]entities.CustomerTokens, error) {
|
||||||
|
var tokensHistory []entities.CustomerTokens
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Where("customer_id = ?", customerID).
|
||||||
|
Order("created_at DESC")
|
||||||
|
|
||||||
|
if limit > 0 {
|
||||||
|
query = query.Limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Find(&tokensHistory).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get customer tokens history: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokensHistory, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,9 @@ type Router struct {
|
|||||||
rewardHandler *handler.RewardHandler
|
rewardHandler *handler.RewardHandler
|
||||||
campaignHandler *handler.CampaignHandler
|
campaignHandler *handler.CampaignHandler
|
||||||
customerAuthHandler *handler.CustomerAuthHandler
|
customerAuthHandler *handler.CustomerAuthHandler
|
||||||
|
customerPointsHandler *handler.CustomerPointsHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config,
|
func NewRouter(cfg *config.Config,
|
||||||
@ -102,7 +104,9 @@ func NewRouter(cfg *config.Config,
|
|||||||
campaignService service.CampaignService,
|
campaignService service.CampaignService,
|
||||||
campaignValidator validator.CampaignValidator,
|
campaignValidator validator.CampaignValidator,
|
||||||
customerAuthService service.CustomerAuthService,
|
customerAuthService service.CustomerAuthService,
|
||||||
customerAuthValidator validator.CustomerAuthValidator) *Router {
|
customerAuthValidator validator.CustomerAuthValidator,
|
||||||
|
customerPointsService service.CustomerPointsService,
|
||||||
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -136,7 +140,9 @@ func NewRouter(cfg *config.Config,
|
|||||||
rewardHandler: handler.NewRewardHandler(rewardService, rewardValidator),
|
rewardHandler: handler.NewRewardHandler(rewardService, rewardValidator),
|
||||||
campaignHandler: handler.NewCampaignHandler(campaignService, campaignValidator),
|
campaignHandler: handler.NewCampaignHandler(campaignService, campaignValidator),
|
||||||
customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator),
|
customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator),
|
||||||
|
customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService),
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
|
customerAuthMiddleware: customerAuthMiddleware,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +187,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
customerAuth.POST("/resend-otp", r.customerAuthHandler.ResendOtp)
|
customerAuth.POST("/resend-otp", r.customerAuthHandler.ResendOtp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Customer authenticated routes
|
||||||
|
customer := v1.Group("/customer")
|
||||||
|
customer.Use(r.customerAuthMiddleware.ValidateCustomerToken())
|
||||||
|
{
|
||||||
|
customer.GET("/points", r.customerPointsHandler.GetCustomerPoints)
|
||||||
|
customer.GET("/tokens", r.customerPointsHandler.GetCustomerTokens)
|
||||||
|
customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet)
|
||||||
|
}
|
||||||
|
|
||||||
organizations := v1.Group("/organizations")
|
organizations := v1.Group("/organizations")
|
||||||
{
|
{
|
||||||
organizations.POST("", r.organizationHandler.CreateOrganization)
|
organizations.POST("", r.organizationHandler.CreateOrganization)
|
||||||
|
|||||||
64
internal/service/customer_points_service.go
Normal file
64
internal/service/customer_points_service.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomerPointsService interface {
|
||||||
|
GetCustomerPoints(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error)
|
||||||
|
GetCustomerTokens(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error)
|
||||||
|
GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type customerPointsService struct {
|
||||||
|
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomerPointsService(customerPointsProcessor *processor.CustomerPointsProcessor) CustomerPointsService {
|
||||||
|
return &customerPointsService{
|
||||||
|
customerPointsProcessor: customerPointsProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customerPointsService) GetCustomerPoints(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error) {
|
||||||
|
if customerID == "" {
|
||||||
|
return nil, fmt.Errorf("customer ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.customerPointsProcessor.GetCustomerTotalPointsAPI(ctx, customerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get customer points: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customerPointsService) GetCustomerTokens(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error) {
|
||||||
|
if customerID == "" {
|
||||||
|
return nil, fmt.Errorf("customer ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.customerPointsProcessor.GetCustomerTotalTokensAPI(ctx, customerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get customer tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customerPointsService) GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
||||||
|
if customerID == "" {
|
||||||
|
return nil, fmt.Errorf("customer ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.customerPointsProcessor.GetCustomerWalletAPI(ctx, customerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get customer wallet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user