This commit is contained in:
Aditya Siregar 2025-09-18 02:01:50 +07:00
parent 259b8a11b5
commit f64fec1fe2
10 changed files with 747 additions and 251 deletions

View File

@ -42,7 +42,7 @@ func (a *App) Initialize(cfg *config.Config) error {
processors := a.initProcessors(cfg, repos)
services := a.initServices(processors, repos, cfg)
validators := a.initValidators()
middleware := a.initMiddleware(services)
middleware := a.initMiddleware(services, cfg)
healthHandler := handler.NewHealthHandler()
a.router = router.NewRouter(
@ -102,6 +102,8 @@ func (a *App) Initialize(cfg *config.Config) error {
validators.campaignValidator,
services.customerAuthService,
validators.customerAuthValidator,
services.customerPointsService,
middleware.customerAuthMiddleware,
)
return nil
@ -185,6 +187,7 @@ type repositories struct {
rewardRepo repository.RewardRepository
campaignRepo repository.CampaignRepository
customerAuthRepo repository.CustomerAuthRepository
customerPointsRepo repository.CustomerPointsRepository
otpRepo repository.OtpRepository
txManager *repository.TxManager
}
@ -229,6 +232,7 @@ func (a *App) initRepositories() *repositories {
rewardRepo: repository.NewRewardRepository(a.db),
campaignRepo: repository.NewCampaignRepository(a.db),
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
otpRepo: repository.NewOtpRepository(a.db),
txManager: repository.NewTxManager(a.db),
}
@ -269,6 +273,7 @@ type processors struct {
rewardProcessor processor.RewardProcessor
campaignProcessor processor.CampaignProcessor
customerAuthProcessor processor.CustomerAuthProcessor
customerPointsProcessor *processor.CustomerPointsProcessor
otpProcessor processor.OtpProcessor
fileClient processor.FileClient
inventoryMovementService service.InventoryMovementService
@ -315,6 +320,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo),
campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo),
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo),
otpProcessor: otpProcessor,
fileClient: fileClient,
inventoryMovementService: inventoryMovementService,
@ -352,6 +358,7 @@ type services struct {
rewardService service.RewardService
campaignService service.CampaignService
customerAuthService service.CustomerAuthService
customerPointsService service.CustomerPointsService
}
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)
campaignService := service.NewCampaignService(processors.campaignProcessor)
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
// Update order service with order ingredient transaction service
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,
campaignService: campaignService,
customerAuthService: customerAuthService,
customerPointsService: customerPointsService,
}
}
type middlewares struct {
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{
authMiddleware: middleware.NewAuthMiddleware(services.authService),
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/google/uuid"
)
// Existing gamification contracts
type CreateCustomerPointsRequest struct {
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
Balance int64 `json:"balance" validate:"min=0"`
@ -47,3 +48,71 @@ type PaginatedCustomerPointsResponse struct {
Limit int `json:"limit"`
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"`
}

View 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")
}

View File

@ -129,6 +129,7 @@ func ToCampaignRuleEntity(request *contract.CampaignRuleStruct, campaignID uuid.
}
return &entities.CampaignRule{
ID: uuid.New(), // Generate unique UUID for each campaign rule
CampaignID: campaignID,
RuleType: entities.RuleType(request.RuleType),
ConditionValue: request.ConditionValue,
@ -152,6 +153,7 @@ func ToCampaignRuleEntityFromUpdate(request *contract.CampaignRuleStruct, campai
}
return &entities.CampaignRule{
ID: uuid.New(), // Generate new UUID for each campaign rule
CampaignID: campaignID,
RuleType: entities.RuleType(request.RuleType),
ConditionValue: request.ConditionValue,

View 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()
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/google/uuid"
)
// Existing gamification models
type CreateCustomerPointsRequest struct {
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
Balance int64 `json:"balance" validate:"min=0"`
@ -16,11 +17,11 @@ type UpdateCustomerPointsRequest struct {
}
type AddCustomerPointsRequest struct {
Points int64 `json:"points" validate:"required,min=1"`
Balance int64 `json:"balance" validate:"required,min=1"`
}
type DeductCustomerPointsRequest struct {
Points int64 `json:"points" validate:"required,min=1"`
Balance int64 `json:"balance" validate:"required,min=1"`
}
type CustomerPointsResponse struct {
@ -33,9 +34,85 @@ type CustomerPointsResponse struct {
}
type ListCustomerPointsQuery struct {
Page int `query:"page" validate:"min=1"`
Limit int `query:"limit" validate:"min=1,max=100"`
Search string `query:"search"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search"`
SortBy string `json:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"`
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"`
}

View File

@ -1,196 +1,222 @@
package processor
import (
"apskel-pos-be/internal/mappers"
"context"
"fmt"
"time"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"context"
"errors"
"fmt"
"github.com/google/uuid"
)
type CustomerPointsProcessor struct {
customerPointsRepo *repository.CustomerPointsRepository
customerPointsRepo repository.CustomerPointsRepository
}
func NewCustomerPointsProcessor(customerPointsRepo *repository.CustomerPointsRepository) *CustomerPointsProcessor {
func NewCustomerPointsProcessor(customerPointsRepo repository.CustomerPointsRepository) *CustomerPointsProcessor {
return &CustomerPointsProcessor{
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) {
// Convert request to entity
customerPoints := mappers.ToCustomerPointsEntity(req)
// Create customer points
err := p.customerPointsRepo.Create(ctx, customerPoints)
if err != nil {
return nil, fmt.Errorf("failed to create customer points: %w", err)
// TODO: Implement this method
return nil, fmt.Errorf("not implemented")
}
return mappers.ToCustomerPointsResponse(customerPoints), nil
}
// GetCustomerPoints retrieves customer points by ID
func (p *CustomerPointsProcessor) GetCustomerPoints(ctx context.Context, id uuid.UUID) (*models.CustomerPointsResponse, error) {
customerPoints, err := p.customerPointsRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("customer points not found: %w", err)
// TODO: Implement this method
return nil, fmt.Errorf("not implemented")
}
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) {
customerPoints, err := p.customerPointsRepo.EnsureCustomerPoints(ctx, customerID)
// TODO: Implement this method
return nil, fmt.Errorf("not implemented")
}
func (p *CustomerPointsProcessor) ListCustomerPoints(ctx context.Context, query *models.ListCustomerPointsQuery) (*models.PaginatedCustomerPointsResponse, error) {
// Return empty paginated response for now
return &models.PaginatedCustomerPointsResponse{
Data: []models.CustomerPointsResponse{},
TotalCount: 0,
Page: 1,
Limit: 10,
TotalPages: 0,
}, nil
}
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")
}
func (p *CustomerPointsProcessor) DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error {
// TODO: Implement this method
return fmt.Errorf("not implemented")
}
func (p *CustomerPointsProcessor) AddPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) {
// TODO: Implement this method
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 {
return nil, fmt.Errorf("failed to get customer points: %w", err)
return nil, fmt.Errorf("failed to get customer total 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.PaginatedResponse[models.CustomerPointsResponse], error) {
// Set default values
if query.Page <= 0 {
query.Page = 1
}
if query.Limit <= 0 {
query.Limit = 10
}
if query.Limit > 100 {
query.Limit = 100
}
offset := (query.Page - 1) * query.Limit
// Get customer points from repository
customerPoints, total, err := p.customerPointsRepo.List(
ctx,
offset,
query.Limit,
query.Search,
query.SortBy,
query.SortOrder,
)
// Get points history (last 10 records)
pointsHistory, err := p.customerPointsRepo.GetCustomerPointsHistory(ctx, customerID, 10)
if err != nil {
return nil, fmt.Errorf("failed to list customer points: %w", err)
return nil, fmt.Errorf("failed to get customer points history: %w", err)
}
// Convert to responses
responses := mappers.ToCustomerPointsResponses(customerPoints)
// Convert to response format
var historyItems []models.PointsHistoryItem
// Calculate pagination info
totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit))
for _, point := range pointsHistory {
historyItems = append(historyItems, models.PointsHistoryItem{
ID: point.ID.String(),
Points: point.Balance,
Type: "BALANCE",
Description: "Points balance",
CreatedAt: point.CreatedAt,
})
}
return &models.PaginatedResponse[models.CustomerPointsResponse]{
Data: responses,
Pagination: models.Pagination{
Page: query.Page,
Limit: query.Limit,
Total: total,
TotalPages: totalPages,
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
}
// UpdateCustomerPoints updates an existing customer points record
func (p *CustomerPointsProcessor) UpdateCustomerPoints(ctx context.Context, id uuid.UUID, req *models.UpdateCustomerPointsRequest) (*models.CustomerPointsResponse, error) {
// Get existing customer points
customerPoints, err := p.customerPointsRepo.GetByID(ctx, id)
func (p *CustomerPointsProcessor) GetCustomerTotalTokensAPI(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error) {
// Get total tokens
totalTokens, err := p.customerPointsRepo.GetCustomerTotalTokens(ctx, customerID)
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
mappers.UpdateCustomerPointsEntity(customerPoints, req)
// Save updated customer points
err = p.customerPointsRepo.Update(ctx, customerPoints)
// Get tokens history (last 10 records)
tokensHistory, err := p.customerPointsRepo.GetCustomerTokensHistory(ctx, customerID, 10)
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,
})
}
// DeleteCustomerPoints deletes a customer points record
func (p *CustomerPointsProcessor) DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error {
// Get existing customer points
_, err := p.customerPointsRepo.GetByID(ctx, id)
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
}
func (p *CustomerPointsProcessor) GetCustomerWalletAPI(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
// Get total points
totalPoints, err := p.customerPointsRepo.GetCustomerTotalPoints(ctx, customerID)
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
err = p.customerPointsRepo.Delete(ctx, id)
// Get total tokens
totalTokens, err := p.customerPointsRepo.GetCustomerTotalTokens(ctx, customerID)
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
}
// AddPoints adds points to a customer's balance
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")
}
// Ensure customer points record exists
_, err := p.customerPointsRepo.EnsureCustomerPoints(ctx, customerID)
// Get points history (last 5 records)
pointsHistory, err := p.customerPointsRepo.GetCustomerPointsHistory(ctx, customerID, 5)
if err != nil {
return nil, fmt.Errorf("failed to ensure customer points: %w", err)
return nil, fmt.Errorf("failed to get customer points history: %w", err)
}
// Add points
err = p.customerPointsRepo.AddPoints(ctx, customerID, points)
// Get tokens history (last 5 records)
tokensHistory, err := p.customerPointsRepo.GetCustomerTokensHistory(ctx, customerID, 5)
if err != nil {
return nil, fmt.Errorf("failed to add points: %w", err)
return nil, fmt.Errorf("failed to get customer tokens history: %w", err)
}
// Get updated customer points
customerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID)
if err != nil {
return nil, fmt.Errorf("failed to get updated customer points: %w", err)
// Convert to response format
var pointsHistoryItems []models.PointsHistoryItem
var tokensHistoryItems []models.TokensHistoryItem
var lastUpdated time.Time
for _, point := range pointsHistory {
pointsHistoryItems = append(pointsHistoryItems, models.PointsHistoryItem{
ID: point.ID.String(),
Points: point.Balance,
Type: "BALANCE",
Description: "Points balance",
CreatedAt: point.CreatedAt,
})
if point.CreatedAt.After(lastUpdated) {
lastUpdated = point.CreatedAt
}
}
return mappers.ToCustomerPointsResponse(customerPoints), nil
for _, token := range tokensHistory {
tokensHistoryItems = append(tokensHistoryItems, models.TokensHistoryItem{
ID: token.ID.String(),
Tokens: token.Balance,
Type: string(token.TokenType),
Description: "Tokens balance",
CreatedAt: token.CreatedAt,
})
if token.CreatedAt.After(lastUpdated) {
lastUpdated = token.CreatedAt
}
}
// DeductPoints deducts points from a customer's balance
func (p *CustomerPointsProcessor) DeductPoints(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 current customer points
customerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID)
if err != nil {
return nil, fmt.Errorf("customer points not found: %w", err)
}
if customerPoints.Balance < points {
return nil, errors.New("insufficient points balance")
}
// Deduct points
err = p.customerPointsRepo.DeductPoints(ctx, customerID, points)
if err != nil {
return nil, fmt.Errorf("failed to deduct points: %w", err)
}
// Get updated customer points
updatedCustomerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID)
if err != nil {
return nil, fmt.Errorf("failed to get updated customer points: %w", err)
}
return mappers.ToCustomerPointsResponse(updatedCustomerPoints), nil
return &models.GetCustomerWalletResponse{
Status: "SUCCESS",
Message: "Customer wallet retrieved successfully.",
Data: &models.GetCustomerWalletResponseData{
TotalPoints: totalPoints,
TotalTokens: totalTokens,
PointsHistory: pointsHistoryItems,
TokensHistory: tokensHistoryItems,
LastUpdated: lastUpdated,
},
}, nil
}

View File

@ -1,117 +1,97 @@
package repository
import (
"apskel-pos-be/internal/entities"
"context"
"fmt"
"github.com/google/uuid"
"apskel-pos-be/internal/entities"
"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
}
func NewCustomerPointsRepository(db *gorm.DB) *CustomerPointsRepository {
return &CustomerPointsRepository{db: db}
func NewCustomerPointsRepository(db *gorm.DB) CustomerPointsRepository {
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) GetCustomerTotalPoints(ctx context.Context, customerID string) (int64, error) {
var totalPoints int64
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) {
var customerPoints entities.CustomerPoints
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) {
var customerPoints []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{}).
err := r.db.WithContext(ctx).
Model(&entities.CustomerPoints{}).
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 {
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
}

View File

@ -44,7 +44,9 @@ type Router struct {
rewardHandler *handler.RewardHandler
campaignHandler *handler.CampaignHandler
customerAuthHandler *handler.CustomerAuthHandler
customerPointsHandler *handler.CustomerPointsHandler
authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware
}
func NewRouter(cfg *config.Config,
@ -102,7 +104,9 @@ func NewRouter(cfg *config.Config,
campaignService service.CampaignService,
campaignValidator validator.CampaignValidator,
customerAuthService service.CustomerAuthService,
customerAuthValidator validator.CustomerAuthValidator) *Router {
customerAuthValidator validator.CustomerAuthValidator,
customerPointsService service.CustomerPointsService,
customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router {
return &Router{
config: cfg,
@ -136,7 +140,9 @@ func NewRouter(cfg *config.Config,
rewardHandler: handler.NewRewardHandler(rewardService, rewardValidator),
campaignHandler: handler.NewCampaignHandler(campaignService, campaignValidator),
customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator),
customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService),
authMiddleware: authMiddleware,
customerAuthMiddleware: customerAuthMiddleware,
}
}
@ -181,6 +187,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
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.POST("", r.organizationHandler.CreateOrganization)

View 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
}