package service import ( "context" "errors" "github.com/google/uuid" "apskel-pos-be/internal/appcontext" "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" ) // CustomerPinService serves the customer's PIN (docs/prd-point-coin.md F11) and the // dashboard's view of it. type CustomerPinService interface { Status(ctx context.Context, customerID uuid.UUID) *contract.Response RequestOtp(ctx context.Context, customerID uuid.UUID, req *contract.RequestPinOtpRequest) *contract.Response CreatePin(ctx context.Context, customerID uuid.UUID, req *contract.CreateCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response ChangePin(ctx context.Context, customerID uuid.UUID, req *contract.ChangeCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response ResetPin(ctx context.Context, customerID uuid.UUID, req *contract.ResetCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response RemovePin(ctx context.Context, apctx *appcontext.ContextInfo, customerID uuid.UUID, req *contract.RemoveCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response ListSecurityEvents(ctx context.Context, apctx *appcontext.ContextInfo, customerID uuid.UUID, page, limit int) *contract.Response } type CustomerPinServiceImpl struct { pins *processor.CustomerPinProcessor } func NewCustomerPinService(pins *processor.CustomerPinProcessor) *CustomerPinServiceImpl { return &CustomerPinServiceImpl{pins: pins} } func (s *CustomerPinServiceImpl) Status(ctx context.Context, customerID uuid.UUID) *contract.Response { status, err := s.pins.Status(ctx, customerID) if err != nil { return PinErrorResponse(err) } return contract.BuildSuccessResponse(status) } func (s *CustomerPinServiceImpl) RequestOtp(ctx context.Context, customerID uuid.UUID, req *contract.RequestPinOtpRequest) *contract.Response { otp, err := s.pins.RequestOtp(ctx, customerID, req.Purpose) if err != nil { return PinErrorResponse(err) } return contract.BuildSuccessResponse(otp) } func (s *CustomerPinServiceImpl) CreatePin(ctx context.Context, customerID uuid.UUID, req *contract.CreateCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response { if err := s.pins.CreatePin(ctx, customerID, req.OtpToken, req.OtpCode, req.Pin, req.ConfirmPin, info); err != nil { return PinErrorResponse(err) } return s.Status(ctx, customerID) } func (s *CustomerPinServiceImpl) ChangePin(ctx context.Context, customerID uuid.UUID, req *contract.ChangeCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response { if err := s.pins.ChangePin(ctx, customerID, req.OldPin, req.Pin, req.ConfirmPin, info); err != nil { return PinErrorResponse(err) } return s.Status(ctx, customerID) } func (s *CustomerPinServiceImpl) ResetPin(ctx context.Context, customerID uuid.UUID, req *contract.ResetCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response { if err := s.pins.ResetPin(ctx, customerID, req.OtpToken, req.OtpCode, req.Pin, req.ConfirmPin, info); err != nil { return PinErrorResponse(err) } return s.Status(ctx, customerID) } func (s *CustomerPinServiceImpl) RemovePin(ctx context.Context, apctx *appcontext.ContextInfo, customerID uuid.UUID, req *contract.RemoveCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response { if err := s.pins.RemovePinByAdmin(ctx, apctx.OrganizationID, customerID, apctx.UserID, req.Reason, info); err != nil { return PinErrorResponse(err) } return contract.BuildSuccessResponse(map[string]interface{}{"message": "PIN removed; the customer has to create a new one"}) } func (s *CustomerPinServiceImpl) ListSecurityEvents(ctx context.Context, apctx *appcontext.ContextInfo, customerID uuid.UUID, page, limit int) *contract.Response { events, err := s.pins.ListEvents(ctx, apctx.OrganizationID, customerID, page, limit) if err != nil { return PinErrorResponse(err) } return contract.BuildSuccessResponse(events) } // PinErrorResponse turns an error from a PIN-guarded action into a response the apps // can act on. A *processor.PinError keeps its code (PIN_NOT_SET, PIN_INVALID, // PIN_LOCKED, TRANSFER_BLOCKED) and puts the attempts left or the time it lifts in the // response data. Other errors map to a validation or server error. func PinErrorResponse(err error) *contract.Response { var pinErr *processor.PinError if errors.As(err, &pinErr) { data := map[string]interface{}{"code": pinErr.Code} switch pinErr.Code { case processor.PinErrInvalid: data["remaining_attempts"] = pinErr.RemainingAttempts case processor.PinErrLocked: data["locked_until"] = pinErr.Until case processor.PinErrTransferBlocked: data["transfer_blocked_until"] = pinErr.Until } return &contract.Response{ Success: false, Data: data, Errors: []*contract.ResponseError{contract.NewResponseError(pinErr.Code, constants.CustomerPinServiceEntity, pinErr.Error())}, } } code := constants.InternalServerErrorCode switch { case errors.Is(err, repository.ErrPinCustomerNotFound): code = constants.NotFoundErrorCode case errors.Is(err, processor.ErrPinOtpTooSoon): code = constants.TooManyRequestsErrorCode case errors.Is(err, processor.ErrInvalidPinInput), errors.Is(err, processor.ErrPinAlreadySet), errors.Is(err, processor.ErrPinOtpInvalid), errors.Is(err, processor.ErrPinNoPhone): code = constants.ValidationErrorCode } return contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(code, constants.CustomerPinServiceEntity, err.Error()), }) }