package adminfinance import ( "context" "errors" "net/url" "strings" "unicode/utf8" "hfb_sys/backend/internal/auditlog" ) var ( ErrDependencyUnavailable = errors.New("dependency unavailable") ErrInvalidManualDisbursement = errors.New("invalid manual disbursement") ErrManualDisbursementNotFound = errors.New("manual disbursement not found") ErrManualDisbursementNotPaid = errors.New("manual disbursement is not paid") ) type Service struct { repo *Repository } func NewService(repo *Repository) *Service { return &Service{repo: repo} } func (s *Service) Dashboard(ctx context.Context, query DashboardQuery) (*DashboardDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.Dashboard(ctx, query) } func (s *Service) Details(ctx context.Context, query DetailQuery) (*PaginatedResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } if query.Page < 1 { query.Page = 1 } if query.PageSize < 1 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } return s.repo.Details(ctx, query) } func (s *Service) Disbursements(ctx context.Context, query DisbursementQuery) (*DisbursementListDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } if query.Page < 1 { query.Page = 1 } if query.PageSize < 1 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } return s.repo.Disbursements(ctx, query) } func (s *Service) CreateManualDisbursement( ctx context.Context, req CreateManualDisbursementRequest, adminID uint64, meta auditlog.Meta, ) (*ManualDisbursementDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } req.Category = strings.TrimSpace(req.Category) req.CustomCategoryName = strings.TrimSpace(req.CustomCategoryName) req.PayeeName = strings.TrimSpace(req.PayeeName) req.Remark = strings.TrimSpace(req.Remark) req.VoucherURL = strings.TrimSpace(req.VoucherURL) if !validManualDisbursementCategory(req.Category) || !validManualDisbursementCustomCategoryName(req.Category, req.CustomCategoryName) || utf8.RuneCountInString(req.PayeeName) < 1 || utf8.RuneCountInString(req.PayeeName) > 100 || req.AmountCent <= 0 || req.PaidAt.IsZero() || utf8.RuneCountInString(req.Remark) < 1 || utf8.RuneCountInString(req.Remark) > 500 || !validManualVoucherURL(req.VoucherURL) { return nil, ErrInvalidManualDisbursement } return s.repo.CreateManualDisbursement(ctx, req, adminID, meta) } func (s *Service) VoidManualDisbursement( ctx context.Context, id uint64, reason string, adminID uint64, meta auditlog.Meta, ) (*ManualDisbursementDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } reason = strings.TrimSpace(reason) if id == 0 || utf8.RuneCountInString(reason) < 1 || utf8.RuneCountInString(reason) > 255 { return nil, ErrInvalidManualDisbursement } return s.repo.VoidManualDisbursement(ctx, id, reason, adminID, meta) } func validManualDisbursementCategory(category string) bool { switch category { case "user_compensation", "seller_supplement", "operating_expense", "channel_fee", "duoduo_deposit_refund", "other", "custom": return true default: return false } } func validManualDisbursementCustomCategoryName(category, name string) bool { if category != "custom" { return name == "" } return utf8.RuneCountInString(name) >= 1 && utf8.RuneCountInString(name) <= 50 } func validManualVoucherURL(raw string) bool { if raw == "" { return true } if len(raw) > 500 { return false } parsed, err := url.Parse(raw) if err != nil || parsed.IsAbs() || parsed.Path != "/api/files/object" { return false } return strings.HasPrefix(parsed.Query().Get("key"), "manual-disbursement/") }