Files
template-vitrine/backend/internal/modules/site/service.go
T
Xor290 919c807004
ci-api / test (push) Failing after 8m7s
ci-web / test (push) Failing after 5m5s
chore: build
2026-09-20 12:18:33 +02:00

208 lines
7.2 KiB
Go

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
}