118 lines
4.4 KiB
Go
118 lines
4.4 KiB
Go
package service
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
|
||
|
|
"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"
|
||
|
|
)
|
||
|
|
|
||
|
|
// LoyaltySettingsService is the dashboard's access to loyalty settings
|
||
|
|
// (docs/prd-point-coin.md F1, F2). Every call is scoped to the caller's organization.
|
||
|
|
type LoyaltySettingsService interface {
|
||
|
|
GetOutletSettings(ctx context.Context, apctx *appcontext.ContextInfo, outletID uuid.UUID) *contract.Response
|
||
|
|
// UpdateOutletSettings applies a JSON body on top of the current settings: fields
|
||
|
|
// left out keep their value, and null clears an optional limit.
|
||
|
|
UpdateOutletSettings(ctx context.Context, apctx *appcontext.ContextInfo, outletID uuid.UUID, body []byte) *contract.Response
|
||
|
|
}
|
||
|
|
|
||
|
|
type LoyaltySettingsServiceImpl struct {
|
||
|
|
settings *processor.LoyaltySettingsProcessor
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewLoyaltySettingsService(settings *processor.LoyaltySettingsProcessor) *LoyaltySettingsServiceImpl {
|
||
|
|
return &LoyaltySettingsServiceImpl{settings: settings}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *LoyaltySettingsServiceImpl) GetOutletSettings(ctx context.Context, apctx *appcontext.ContextInfo, outletID uuid.UUID) *contract.Response {
|
||
|
|
current, err := s.settings.OutletForOrganization(ctx, apctx.OrganizationID, outletID)
|
||
|
|
if err != nil {
|
||
|
|
return loyaltyErrorResponse(err)
|
||
|
|
}
|
||
|
|
view, err := s.outletView(ctx, apctx.OrganizationID, outletID, *current, nil)
|
||
|
|
if err != nil {
|
||
|
|
return loyaltyErrorResponse(err)
|
||
|
|
}
|
||
|
|
return contract.BuildSuccessResponse(view)
|
||
|
|
}
|
||
|
|
|
||
|
|
// outletSettingsInput is what PUT accepts: the settings, plus the read-only fields of
|
||
|
|
// the GET response so a client can send back what it received. Those are ignored.
|
||
|
|
type outletSettingsInput struct {
|
||
|
|
*models.OutletLoyaltySettings
|
||
|
|
OutletID json.RawMessage `json:"outlet_id"`
|
||
|
|
PointValue json.RawMessage `json:"point_value"`
|
||
|
|
PointCashbackPercent json.RawMessage `json:"point_cashback_percent"`
|
||
|
|
Changes json.RawMessage `json:"changes"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *LoyaltySettingsServiceImpl) UpdateOutletSettings(ctx context.Context, apctx *appcontext.ContextInfo, outletID uuid.UUID, body []byte) *contract.Response {
|
||
|
|
current, err := s.settings.OutletForOrganization(ctx, apctx.OrganizationID, outletID)
|
||
|
|
if err != nil {
|
||
|
|
return loyaltyErrorResponse(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
next := *current
|
||
|
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
||
|
|
decoder.DisallowUnknownFields()
|
||
|
|
if err := decoder.Decode(&outletSettingsInput{OutletLoyaltySettings: &next}); err != nil {
|
||
|
|
return contract.BuildErrorResponse([]*contract.ResponseError{
|
||
|
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.LoyaltySettingsServiceEntity, fmt.Sprintf("invalid request body: %v", err)),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
changes, err := s.settings.UpdateOutlet(ctx, apctx.OrganizationID, outletID, apctx.UserID, next)
|
||
|
|
if err != nil {
|
||
|
|
return loyaltyErrorResponse(err)
|
||
|
|
}
|
||
|
|
saved, err := s.settings.Outlet(ctx, outletID)
|
||
|
|
if err != nil {
|
||
|
|
return loyaltyErrorResponse(err)
|
||
|
|
}
|
||
|
|
view, err := s.outletView(ctx, apctx.OrganizationID, outletID, *saved, changes)
|
||
|
|
if err != nil {
|
||
|
|
return loyaltyErrorResponse(err)
|
||
|
|
}
|
||
|
|
return contract.BuildSuccessResponse(view)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *LoyaltySettingsServiceImpl) outletView(ctx context.Context, organizationID, outletID uuid.UUID, settings models.OutletLoyaltySettings, changes []models.LoyaltySettingChange) (*models.OutletLoyaltySettingsView, error) {
|
||
|
|
pointValue, err := s.settings.PointValue(ctx, organizationID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if changes == nil {
|
||
|
|
changes = []models.LoyaltySettingChange{}
|
||
|
|
}
|
||
|
|
return &models.OutletLoyaltySettingsView{
|
||
|
|
OutletID: outletID,
|
||
|
|
OutletLoyaltySettings: settings,
|
||
|
|
PointValue: pointValue,
|
||
|
|
PointCashbackPercent: models.LoyaltyCashbackPercent(settings.Point.EarnValue, pointValue, settings.Point.EarnPerAmount),
|
||
|
|
Changes: changes,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func loyaltyErrorResponse(err error) *contract.Response {
|
||
|
|
code, message := constants.InternalServerErrorCode, err.Error()
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, repository.ErrLoyaltyOutletNotFound):
|
||
|
|
code, message = constants.NotFoundErrorCode, "outlet not found"
|
||
|
|
case errors.Is(err, processor.ErrInvalidLoyaltySettings):
|
||
|
|
code = constants.ValidationErrorCode
|
||
|
|
}
|
||
|
|
return contract.BuildErrorResponse([]*contract.ResponseError{
|
||
|
|
contract.NewResponseError(code, constants.LoyaltySettingsServiceEntity, message),
|
||
|
|
})
|
||
|
|
}
|