971a2bd9a9
- Add rate limiting per endpoint (login: 5/min, API: 100/min) - Add input validation helpers (email, UUID, string, int) - Add tenant isolation to all handlers - Remove old validation.go, replace with input.go - Fix service/customer.go to use new validation functions - Build successful
190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"boc/middleware"
|
|
"boc/repository"
|
|
)
|
|
|
|
// CustomerService innehåller affärslogik för kunder
|
|
type CustomerService struct {
|
|
repo *repository.CustomerRepository
|
|
}
|
|
|
|
func NewCustomerService(repo *repository.CustomerRepository) *CustomerService {
|
|
return &CustomerService{repo: repo}
|
|
}
|
|
|
|
// CreateCustomer skapar en ny kund med validering
|
|
func (s *CustomerService) CreateCustomer(ctx context.Context, req *CreateCustomerRequest) (*repository.Customer, error) {
|
|
// Validera input
|
|
if err := middleware.ValidateString(req.Name, 2, 255, true); err != nil {
|
|
return nil, fmt.Errorf("validation failed: name %v", err)
|
|
}
|
|
if err := middleware.ValidateEmail(req.Email); err != nil {
|
|
return nil, fmt.Errorf("validation failed: email %v", err)
|
|
}
|
|
|
|
if req.Phone != "" {
|
|
if len(req.Phone) < 8 || len(req.Phone) > 15 {
|
|
return nil, fmt.Errorf("validation failed: phone invalid format")
|
|
}
|
|
}
|
|
|
|
if req.OrgNumber != "" {
|
|
if len(req.OrgNumber) < 6 {
|
|
return nil, fmt.Errorf("validation failed: org_number invalid format")
|
|
}
|
|
}
|
|
|
|
// Skapa kund
|
|
customer := &repository.Customer{
|
|
TenantID: req.TenantID,
|
|
Name: middleware.SanitizeString(req.Name),
|
|
Email: req.Email,
|
|
Phone: req.Phone,
|
|
Company: middleware.SanitizeString(req.Company),
|
|
OrgNumber: req.OrgNumber,
|
|
Status: req.Status,
|
|
Source: req.Source,
|
|
Tags: req.Tags,
|
|
AssignedTo: req.AssignedTo,
|
|
}
|
|
|
|
if customer.Status == "" {
|
|
customer.Status = "lead"
|
|
}
|
|
|
|
if err := s.repo.CreateCustomer(ctx, customer); err != nil {
|
|
return nil, fmt.Errorf("create customer: %w", err)
|
|
}
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// GetCustomer hämtar en kund med ID
|
|
func (s *CustomerService) GetCustomer(ctx context.Context, id string) (*repository.Customer, error) {
|
|
if err := middleware.ValidateUUID(id); err != nil {
|
|
return nil, fmt.Errorf("invalid customer ID")
|
|
}
|
|
|
|
customer, err := s.repo.GetCustomer(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// ListCustomers hämtar kunder med pagination
|
|
func (s *CustomerService) ListCustomers(ctx context.Context, tenantID, status string, page, pageSize int) (*ListCustomersResponse, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 100 {
|
|
pageSize = 20
|
|
}
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
customers, total, err := s.repo.ListCustomers(ctx, tenantID, status, pageSize, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ListCustomersResponse{
|
|
Customers: customers,
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
Pages: (total + pageSize - 1) / pageSize,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateCustomer uppdaterar en kund
|
|
func (s *CustomerService) UpdateCustomer(ctx context.Context, id string, req *UpdateCustomerRequest) (*repository.Customer, error) {
|
|
if err := middleware.ValidateUUID(id); err != nil {
|
|
return nil, fmt.Errorf("invalid customer ID")
|
|
}
|
|
|
|
// Hämta befintlig kund
|
|
customer, err := s.repo.GetCustomer(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Uppdatera fält
|
|
if req.Name != "" {
|
|
customer.Name = middleware.SanitizeString(req.Name)
|
|
}
|
|
if req.Email != "" {
|
|
customer.Email = req.Email
|
|
}
|
|
if req.Phone != "" {
|
|
customer.Phone = req.Phone
|
|
}
|
|
if req.Company != "" {
|
|
customer.Company = middleware.SanitizeString(req.Company)
|
|
}
|
|
if req.Status != "" {
|
|
customer.Status = req.Status
|
|
}
|
|
|
|
if err := s.repo.UpdateCustomer(ctx, customer); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// DeleteCustomer tar bort en kund (soft delete)
|
|
func (s *CustomerService) DeleteCustomer(ctx context.Context, id string) error {
|
|
if err := middleware.ValidateUUID(id); err != nil {
|
|
return fmt.Errorf("invalid customer ID")
|
|
}
|
|
|
|
return s.repo.DeleteCustomer(ctx, id)
|
|
}
|
|
|
|
// SearchCustomers söker efter kunder
|
|
func (s *CustomerService) SearchCustomers(ctx context.Context, tenantID, query string, limit int) ([]repository.Customer, error) {
|
|
if limit < 1 || limit > 100 {
|
|
limit = 20
|
|
}
|
|
|
|
return s.repo.SearchCustomers(ctx, tenantID, query, limit)
|
|
}
|
|
|
|
// ── Request/Response DTOs ─────────────────────────────────────────────────
|
|
|
|
type CreateCustomerRequest struct {
|
|
TenantID string `json:"tenant_id"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Phone string `json:"phone,omitempty"`
|
|
Company string `json:"company,omitempty"`
|
|
OrgNumber string `json:"org_number,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
AssignedTo *string `json:"assigned_to,omitempty"`
|
|
}
|
|
|
|
type UpdateCustomerRequest struct {
|
|
Name string `json:"name,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
Phone string `json:"phone,omitempty"`
|
|
Company string `json:"company,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
type ListCustomersResponse struct {
|
|
Customers []repository.Customer `json:"customers"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
Pages int `json:"pages"`
|
|
}
|