GET /customer/wallet now reads the EnakPoint & EnakCoin wallet (docs/prd-point-coin.md F6, PC-106): spendable point and coin balances, the rupiah value of one EnakPoint and of the balance, the nearest day each currency loses balance (grouped by Asia/Jakarta day), and recent ledger rows. The fields of the pre-wallet response stay, filled from the wallet, so app versions that read them keep working. Adds GET /customer/wallet/transactions with pagination and filters for currency, one or more types, and an inclusive date range. Each row shows where the value came from (additions) or went to (deductions) as in §8.1, and additions list their lots and earliest expiry. The counterparty id, the admin and the metadata are left out; the description already carries the masked name. A malformed query answers 400, a missing customer 404. /customer/points and /customer/tokens keep their shape and now read the wallet too, so customer_points_repository is no longer used for balances. Balances are what the customer can spend: lots that have expired but that the expiry job has not processed are not counted. The point value is read from organization_settings (loyalty.point.value, default 1) through a small repository that the typed settings reader in PC-109 will build on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
7.4 KiB
Go
227 lines
7.4 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/repository"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type CustomerPointsProcessor struct {
|
|
walletQuery *WalletQueryProcessor
|
|
gameRepo *repository.GameRepository
|
|
}
|
|
|
|
func NewCustomerPointsProcessor(walletQuery *WalletQueryProcessor, gameRepo *repository.GameRepository) *CustomerPointsProcessor {
|
|
return &CustomerPointsProcessor{
|
|
walletQuery: walletQuery,
|
|
gameRepo: gameRepo,
|
|
}
|
|
}
|
|
|
|
// Existing gamification methods - placeholder implementations
|
|
func (p *CustomerPointsProcessor) CreateCustomerPoints(ctx context.Context, req *models.CreateCustomerPointsRequest) (*models.CustomerPointsResponse, error) {
|
|
// TODO: Implement this method
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (p *CustomerPointsProcessor) GetCustomerPoints(ctx context.Context, id uuid.UUID) (*models.CustomerPointsResponse, error) {
|
|
// TODO: Implement this method
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (p *CustomerPointsProcessor) GetCustomerPointsByCustomerID(ctx context.Context, customerID uuid.UUID) (*models.CustomerPointsResponse, error) {
|
|
// 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")
|
|
}
|
|
|
|
// The three endpoints below are the customer app's balance screens. They read the
|
|
// EnakPoint & EnakCoin wallet; customer_points and customer_tokens are no longer used
|
|
// for balances (docs/prd-point-coin.md §9).
|
|
|
|
func (p *CustomerPointsProcessor) GetCustomerTotalPointsAPI(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error) {
|
|
id, err := parseWalletCustomerID(customerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := p.walletQuery.LegacyPoints(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get customer points: %w", err)
|
|
}
|
|
return &models.GetCustomerPointsResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Customer points retrieved successfully.",
|
|
Data: data,
|
|
}, nil
|
|
}
|
|
|
|
func (p *CustomerPointsProcessor) GetCustomerTotalTokensAPI(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error) {
|
|
id, err := parseWalletCustomerID(customerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := p.walletQuery.LegacyTokens(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get customer tokens: %w", err)
|
|
}
|
|
return &models.GetCustomerTokensResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Customer tokens retrieved successfully.",
|
|
Data: data,
|
|
}, nil
|
|
}
|
|
|
|
func (p *CustomerPointsProcessor) GetCustomerWalletAPI(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error) {
|
|
id, err := parseWalletCustomerID(customerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := p.walletQuery.Summary(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get customer wallet: %w", err)
|
|
}
|
|
return &models.GetCustomerWalletResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Customer wallet retrieved successfully.",
|
|
Data: data,
|
|
}, nil
|
|
}
|
|
|
|
func (p *CustomerPointsProcessor) GetCustomerWalletTransactionsAPI(ctx context.Context, customerID string, query models.ListCustomerWalletTransactionsQuery) (*models.PaginatedResponse[models.CustomerWalletTransaction], error) {
|
|
id, err := parseWalletCustomerID(customerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.walletQuery.Transactions(ctx, id, query)
|
|
}
|
|
|
|
func parseWalletCustomerID(customerID string) (uuid.UUID, error) {
|
|
id, err := uuid.Parse(customerID)
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("%w: invalid customer id", ErrInvalidWalletQuery)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// GetCustomerGamesAPI gets active SPIN games for customers
|
|
func (p *CustomerPointsProcessor) GetCustomerGamesAPI(ctx context.Context) (*models.GetCustomerGamesResponse, error) {
|
|
// Get active SPIN games
|
|
games, err := p.gameRepo.GetActiveSpinGames(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get active SPIN games: %w", err)
|
|
}
|
|
|
|
// Convert to response format
|
|
var gameResponses []models.CustomerGameResponse
|
|
for _, game := range games {
|
|
var prizeResponses []models.CustomerGamePrizeResponse
|
|
for _, prize := range game.Prizes {
|
|
prizeResponses = append(prizeResponses, models.CustomerGamePrizeResponse{
|
|
ID: prize.ID,
|
|
GameID: prize.GameID,
|
|
Name: prize.Name,
|
|
Image: prize.Image,
|
|
Metadata: (*map[string]interface{})(&prize.Metadata),
|
|
CreatedAt: prize.CreatedAt,
|
|
UpdatedAt: prize.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
gameResponses = append(gameResponses, models.CustomerGameResponse{
|
|
ID: game.ID,
|
|
Name: game.Name,
|
|
Type: string(game.Type),
|
|
IsActive: game.IsActive,
|
|
Metadata: (*map[string]interface{})(&game.Metadata),
|
|
Prizes: prizeResponses,
|
|
CreatedAt: game.CreatedAt,
|
|
UpdatedAt: game.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
return &models.GetCustomerGamesResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Customer games retrieved successfully.",
|
|
Data: &models.GetCustomerGamesResponseData{
|
|
Games: gameResponses,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// GetFerrisWheelGameAPI gets the Ferris Wheel game for customers
|
|
func (p *CustomerPointsProcessor) GetFerrisWheelGameAPI(ctx context.Context) (*models.GetFerrisWheelGameResponse, error) {
|
|
// Get Ferris Wheel game
|
|
game, err := p.gameRepo.GetFerrisWheelGame(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get Ferris Wheel game: %w", err)
|
|
}
|
|
|
|
// Convert prizes to response format
|
|
var prizeResponses []models.CustomerGamePrizeResponse
|
|
for _, prize := range game.Prizes {
|
|
prizeResponses = append(prizeResponses, models.CustomerGamePrizeResponse{
|
|
ID: prize.ID,
|
|
GameID: prize.GameID,
|
|
Name: prize.Name,
|
|
Image: prize.Image,
|
|
Metadata: (*map[string]interface{})(&prize.Metadata),
|
|
CreatedAt: prize.CreatedAt,
|
|
UpdatedAt: prize.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
// Convert game to response format
|
|
gameResponse := models.CustomerGameResponse{
|
|
ID: game.ID,
|
|
Name: game.Name,
|
|
Type: string(game.Type),
|
|
IsActive: game.IsActive,
|
|
Metadata: (*map[string]interface{})(&game.Metadata),
|
|
Prizes: prizeResponses,
|
|
CreatedAt: game.CreatedAt,
|
|
UpdatedAt: game.UpdatedAt,
|
|
}
|
|
|
|
return &models.GetFerrisWheelGameResponse{
|
|
Status: "SUCCESS",
|
|
Message: "Ferris Wheel game retrieved successfully.",
|
|
Data: &models.GetFerrisWheelGameResponseData{
|
|
Game: gameResponse,
|
|
},
|
|
}, nil
|
|
}
|