package site import ( "context" "github.com/google/uuid" ) type Service struct { repo Repository } func NewService(repo Repository) *Service { return &Service{repo: repo} } func (s *Service) Get(ctx context.Context) (*Settings, error) { return s.repo.Get(ctx) } func (s *Service) Update(ctx context.Context, name, description string, ordersEnabled bool) (*Settings, error) { settings := &Settings{Name: name, Description: description, OrdersEnabled: ordersEnabled} if err := s.repo.Update(ctx, settings); err != nil { return nil, err } return s.repo.Get(ctx) } // AppearanceInput is updated independently from site identity (name/slug): // the "Appearance" admin page only ever touches these fields, so it must // not be blocked by identity validation (e.g. an unset slug) or clobber // name/description/slug on save. Each field is a pointer so the admin can // change a single option (e.g. just the product layout) without resending // every color -- nil fields keep their current, already-saved value. type AppearanceInput struct { HeaderBgColor *string HeaderTextColor *string BodyBgColor *string BodyTextColor *string FooterBgColor *string FooterTextColor *string AccentColor *string ProductLayout *string ProductColumns *int ProductScroll *string ContactCardTransparent *bool ContactCardBgColor *string // LogoMediaIDSet/HeroMediaIDSet distinguish "the admin didn't touch // this field" (false: keep the current value) from "the admin // explicitly chose an image, possibly clearing it to nil" (true: use // the ID as-is, nil included) -- same reasoning as // CustomerAuthInput.VerificationContactLinkSet. LogoMediaIDSet bool LogoMediaID *uuid.UUID HeroMediaIDSet bool HeroMediaID *uuid.UUID HeroPages *string } func (s *Service) UpdateAppearance(ctx context.Context, in AppearanceInput) (*Settings, error) { current, err := s.repo.Get(ctx) if err != nil { return nil, err } settings := &Settings{ HeaderBgColor: applyIfSet(in.HeaderBgColor, current.HeaderBgColor), HeaderTextColor: applyIfSet(in.HeaderTextColor, current.HeaderTextColor), BodyBgColor: applyIfSet(in.BodyBgColor, current.BodyBgColor), BodyTextColor: applyIfSet(in.BodyTextColor, current.BodyTextColor), FooterBgColor: applyIfSet(in.FooterBgColor, current.FooterBgColor), FooterTextColor: applyIfSet(in.FooterTextColor, current.FooterTextColor), AccentColor: applyIfSet(in.AccentColor, current.AccentColor), ProductLayout: applyIfSet(in.ProductLayout, current.ProductLayout), ProductColumns: applyIntIfSet(in.ProductColumns, current.ProductColumns), ProductScroll: applyIfSet(in.ProductScroll, current.ProductScroll), ContactCardTransparent: applyBoolIfSet(in.ContactCardTransparent, current.ContactCardTransparent), ContactCardBgColor: applyIfSet(in.ContactCardBgColor, current.ContactCardBgColor), LogoMediaID: current.LogoMediaID, HeroMediaID: current.HeroMediaID, HeroPages: applyIfSet(in.HeroPages, current.HeroPages), } if in.LogoMediaIDSet { settings.LogoMediaID = in.LogoMediaID } if in.HeroMediaIDSet { settings.HeroMediaID = in.HeroMediaID } if err := s.repo.UpdateAppearance(ctx, settings); err != nil { return nil, err } return s.repo.Get(ctx) } func applyIfSet(value *string, fallback string) string { if value == nil { return fallback } return *value } func applyIntIfSet(value *int, fallback int) int { if value == nil { return fallback } return *value } // CustomerAuthInput is updated independently from identity/appearance, same // reasoning as AppearanceInput: the "Customer accounts" admin page only // ever touches these fields. LoginEnabled and RegistrationEnabled are two // independent switches (e.g. an invite-only shop can enable login while // keeping registration off), so each is its own optional pointer. type CustomerAuthInput struct { LoginEnabled *bool RegistrationEnabled *bool VerificationRequired *bool // VerificationContactLinkSet distinguishes "the admin didn't touch this // field" (false: keep the current value) from "the admin explicitly // chose a link, possibly clearing it to nil" (true: use // VerificationContactLinkID as-is, nil included). VerificationContactLinkSet bool VerificationContactLinkID *uuid.UUID } func (s *Service) UpdateCustomerAuth(ctx context.Context, in CustomerAuthInput) (*Settings, error) { current, err := s.repo.Get(ctx) if err != nil { return nil, err } settings := &Settings{ CustomerLoginEnabled: applyBoolIfSet(in.LoginEnabled, current.CustomerLoginEnabled), CustomerRegistrationEnabled: applyBoolIfSet(in.RegistrationEnabled, current.CustomerRegistrationEnabled), CustomerVerificationRequired: applyBoolIfSet(in.VerificationRequired, current.CustomerVerificationRequired), VerificationContactLinkID: current.VerificationContactLinkID, } if in.VerificationContactLinkSet { settings.VerificationContactLinkID = in.VerificationContactLinkID } if err := s.repo.UpdateCustomerAuth(ctx, settings); err != nil { return nil, err } return s.repo.Get(ctx) } func applyBoolIfSet(value *bool, fallback bool) bool { if value == nil { return fallback } return *value } // CustomerLoginEnabled is consumed by auth.CustomerHandler to gate the // customer login page/endpoint behind the admin's toggle. func (s *Service) CustomerLoginEnabled(ctx context.Context) (bool, error) { settings, err := s.repo.Get(ctx) if err != nil { return false, err } return settings.CustomerLoginEnabled, nil } // CustomerRegistrationEnabled is consumed by auth.CustomerHandler to gate // the customer registration page/endpoint behind the admin's toggle, // independently of CustomerLoginEnabled. func (s *Service) CustomerRegistrationEnabled(ctx context.Context) (bool, error) { settings, err := s.repo.Get(ctx) if err != nil { return false, err } return settings.CustomerRegistrationEnabled, nil } // CustomerAccountsAvailable is consumed by users.Service.Create to prevent // creating customer-role users when neither login nor registration is // enabled -- such an account could never be used to sign in. func (s *Service) CustomerAccountsAvailable(ctx context.Context) (bool, error) { settings, err := s.repo.Get(ctx) if err != nil { return false, err } return settings.CustomerLoginEnabled || settings.CustomerRegistrationEnabled, nil } // CustomerCheckoutSettings is consumed by orders.RequireAccountsEnabled / // RequireApprovedVerificationIfNeeded to gate order creation. func (s *Service) CustomerCheckoutSettings(ctx context.Context) (accountsEnabled, verificationRequired bool, err error) { settings, err := s.repo.Get(ctx) if err != nil { return false, false, err } return settings.CustomerLoginEnabled, settings.CustomerVerificationRequired, nil } // OrdersEnabled is consumed by orders.RequireOrdersEnabled: the site-wide // "vitrine vs boutique" switch, checked before any of the customer-account // gates so a pure showcase site never has to think about accounts at all. func (s *Service) OrdersEnabled(ctx context.Context) (bool, error) { settings, err := s.repo.Get(ctx) if err != nil { return false, err } return settings.OrdersEnabled, nil }