package listing import ( "encoding/json" "errors" "strconv" "strings" ) var ( ErrDependencyUnavailable = errors.New("dependency unavailable") ErrInvalidInput = errors.New("invalid listing input") ErrListingLocked = errors.New("listing locked") ErrMissingTitle = errors.New("missing listing title") ErrMissingServerRegion = errors.New("missing server region") ErrInvalidPrice = errors.New("invalid listing price") ErrInvalidDeposit = errors.New("invalid listing deposit") ErrInvalidHafCoin = errors.New("invalid haf coin amount") ErrMissingScreenshot = errors.New("missing screenshot") ) type Service struct { repo *Repository config ConfigReader } type ConfigReader interface { FindValue(key string) (string, error) } const ( reviewRequiredConfigKey = "listing.review_required" publishOptionsConfigKey = "listing.publish_options" defaultFireLevelMin = 38 ) type FireLevelTooLowError struct { Min int } func (e FireLevelTooLowError) Error() string { return "fire level too low" } func NewService(repo *Repository, config ConfigReader) *Service { return &Service{repo: repo, config: config} } func (s *Service) Create(ownerID uint64, req CreateRequest) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } rules, err := s.publishRules() if err != nil { return nil, err } if err := validateRequest(req, rules); err != nil { return nil, err } reviewRequired, err := s.reviewRequired() if err != nil { return nil, err } return s.repo.Create(ownerID, req, reviewRequired) } func (s *Service) Update(ownerID uint64, id uint64, req UpdateRequest) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } rules, err := s.publishRules() if err != nil { return nil, err } if err := validateRequest(req, rules); err != nil { return nil, err } reviewRequired, err := s.reviewRequired() if err != nil { return nil, err } return s.repo.Update(ownerID, id, req, reviewRequired) } func (s *Service) SubmitReview(ownerID uint64, id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } reviewRequired, err := s.reviewRequired() if err != nil { return nil, err } return s.repo.SubmitReview(ownerID, id, reviewRequired) } func (s *Service) ListPendingReview() ([]ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.ListPendingReview() } func (s *Service) ListAdmin(query AdminListQuery) ([]ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.ListAdmin(query) } func (s *Service) FindAdmin(id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.FindAdmin(id) } func (s *Service) AdminOffline(adminID uint64, id uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } if req.Reason == "" { return nil, ErrInvalidInput } return s.repo.AdminOffline(adminID, id, req, meta) } func (s *Service) AdminMarkAbnormal(adminID uint64, id uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } if req.Reason == "" { return nil, ErrInvalidInput } return s.repo.AdminMarkAbnormal(adminID, id, req, meta) } func (s *Service) Approve(id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.Approve(id) } func (s *Service) Reject(id uint64, req ReviewRequest) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } if req.Reason == "" { return nil, ErrInvalidInput } return s.repo.Reject(id, req) } func (s *Service) Offline(ownerID uint64, id uint64) error { if s.repo == nil { return ErrDependencyUnavailable } return s.repo.Offline(ownerID, id) } func (s *Service) ListPublic() ([]ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.ListPublic() } func (s *Service) ListMine(ownerID uint64) ([]ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.ListMine(ownerID) } func (s *Service) FindPublic(id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.FindPublic(id) } func (s *Service) FindMine(ownerID uint64, id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.FindMine(ownerID, id) } type publishRules struct { FireLevelMin int } func validateRequest(req CreateRequest, rules publishRules) error { if strings.TrimSpace(req.Title) == "" { return ErrMissingTitle } if strings.TrimSpace(req.ServerRegion) == "" { return ErrMissingServerRegion } if req.PriceHourly <= 0 { return ErrInvalidPrice } if req.DepositAmount < 0 { return ErrInvalidDeposit } if req.HafCoinAmount < 0 { return ErrInvalidHafCoin } if !hasScreenshotURL(req.ScreenshotURLS) { return ErrMissingScreenshot } if fireLevel, ok := readFireLevel(req.AssetSummary); ok && fireLevel < rules.FireLevelMin { return FireLevelTooLowError{Min: rules.FireLevelMin} } return nil } func readFireLevel(summary map[string]any) (int, bool) { if summary == nil { return 0, false } value, ok := summary["fire_level"] if !ok { return 0, false } switch current := value.(type) { case float64: return int(current), true case int: return current, true case string: parsed, err := strconv.Atoi(strings.TrimSpace(current)) return parsed, err == nil default: return 0, false } } func hasScreenshotURL(urls []string) bool { for _, url := range urls { if strings.TrimSpace(url) != "" { return true } } return false } func (s *Service) reviewRequired() (bool, error) { if s.config == nil { return false, nil } value, err := s.config.FindValue(reviewRequiredConfigKey) if err != nil { return false, err } required, err := strconv.ParseBool(strings.TrimSpace(value)) if err != nil { return false, nil } return required, nil } func (s *Service) publishRules() (publishRules, error) { rules := publishRules{FireLevelMin: defaultFireLevelMin} if s.config == nil { return rules, nil } value, err := s.config.FindValue(publishOptionsConfigKey) if err != nil { return rules, err } var raw struct { FireLevelMin int `json:"fire_level_min"` } if err := json.Unmarshal([]byte(value), &raw); err != nil { return rules, nil } if raw.FireLevelMin > 0 { rules.FireLevelMin = raw.FireLevelMin } return rules, nil }