2026-09-30 12:09:53 +07:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
|
|
|
|
"apskel-pos-be/internal/constants"
|
|
|
|
|
"apskel-pos-be/internal/contract"
|
|
|
|
|
"apskel-pos-be/internal/models"
|
|
|
|
|
"apskel-pos-be/internal/processor"
|
|
|
|
|
"apskel-pos-be/internal/repository"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CustomerWalletService moves balance on the customer's own request: exchanging
|
2026-09-30 12:14:32 +07:00
|
|
|
// EnakCoin into EnakPoint and sending either to another customer
|
|
|
|
|
// (docs/prd-point-coin.md F4, F5).
|
2026-09-30 12:09:53 +07:00
|
|
|
type CustomerWalletService interface {
|
|
|
|
|
PreviewExchange(ctx context.Context, customerID uuid.UUID, coins int64) *contract.Response
|
|
|
|
|
Exchange(ctx context.Context, customerID uuid.UUID, req *contract.ExchangeCoinsRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response
|
2026-09-30 12:14:32 +07:00
|
|
|
TransferRecipient(ctx context.Context, customerID uuid.UUID, phoneNumber string) *contract.Response
|
|
|
|
|
Transfer(ctx context.Context, customerID uuid.UUID, req *contract.TransferWalletRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response
|
2026-09-30 12:09:53 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CustomerWalletServiceImpl struct {
|
|
|
|
|
exchanges *processor.WalletExchangeProcessor
|
2026-09-30 12:14:32 +07:00
|
|
|
transfers *processor.WalletTransferProcessor
|
2026-09-30 12:09:53 +07:00
|
|
|
}
|
|
|
|
|
|
2026-09-30 12:14:32 +07:00
|
|
|
func NewCustomerWalletService(exchanges *processor.WalletExchangeProcessor, transfers *processor.WalletTransferProcessor) *CustomerWalletServiceImpl {
|
|
|
|
|
return &CustomerWalletServiceImpl{exchanges: exchanges, transfers: transfers}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *CustomerWalletServiceImpl) TransferRecipient(ctx context.Context, customerID uuid.UUID, phoneNumber string) *contract.Response {
|
|
|
|
|
recipient, err := s.transfers.Recipient(ctx, customerID, phoneNumber)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return walletMoveErrorResponse(err)
|
|
|
|
|
}
|
|
|
|
|
return contract.BuildSuccessResponse(recipient)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *CustomerWalletServiceImpl) Transfer(ctx context.Context, customerID uuid.UUID, req *contract.TransferWalletRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response {
|
|
|
|
|
result, err := s.transfers.Transfer(ctx, customerID, models.WalletTransfer{
|
|
|
|
|
Currency: req.Currency,
|
|
|
|
|
Amount: req.Amount,
|
|
|
|
|
RecipientPhone: req.RecipientPhone,
|
|
|
|
|
}, req.Pin, idempotencyKey, info)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return walletMoveErrorResponse(err)
|
|
|
|
|
}
|
|
|
|
|
return contract.BuildSuccessResponse(result)
|
2026-09-30 12:09:53 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *CustomerWalletServiceImpl) PreviewExchange(ctx context.Context, customerID uuid.UUID, coins int64) *contract.Response {
|
|
|
|
|
preview, err := s.exchanges.Preview(ctx, customerID, coins)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return walletMoveErrorResponse(err)
|
|
|
|
|
}
|
|
|
|
|
return contract.BuildSuccessResponse(preview)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *CustomerWalletServiceImpl) Exchange(ctx context.Context, customerID uuid.UUID, req *contract.ExchangeCoinsRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response {
|
|
|
|
|
result, err := s.exchanges.Exchange(ctx, customerID, req.Coins, req.Pin, idempotencyKey, info)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return walletMoveErrorResponse(err)
|
|
|
|
|
}
|
|
|
|
|
return contract.BuildSuccessResponse(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// walletMoveErrorResponse keeps the PIN codes the apps act on, and tells a refused
|
|
|
|
|
// request apart from a server failure.
|
|
|
|
|
func walletMoveErrorResponse(err error) *contract.Response {
|
|
|
|
|
var pinErr *processor.PinError
|
|
|
|
|
if errors.As(err, &pinErr) {
|
|
|
|
|
return PinErrorResponse(err)
|
|
|
|
|
}
|
|
|
|
|
code := constants.InternalServerErrorCode
|
|
|
|
|
switch {
|
2026-09-30 12:14:32 +07:00
|
|
|
case errors.Is(err, repository.ErrWalletNotFound),
|
|
|
|
|
errors.Is(err, processor.ErrWalletRecipientNotFound):
|
2026-09-30 12:09:53 +07:00
|
|
|
code = constants.NotFoundErrorCode
|
|
|
|
|
case errors.Is(err, processor.ErrWalletMoveRejected),
|
|
|
|
|
errors.Is(err, processor.ErrWalletIdempotencyConflict),
|
|
|
|
|
errors.Is(err, processor.ErrWalletInvalidEntry):
|
|
|
|
|
code = constants.ValidationErrorCode
|
|
|
|
|
}
|
|
|
|
|
return contract.BuildErrorResponse([]*contract.ResponseError{
|
|
|
|
|
contract.NewResponseError(code, constants.WalletServiceEntity, err.Error()),
|
|
|
|
|
})
|
|
|
|
|
}
|