add service

This commit is contained in:
vallyenfail
2026-01-17 17:39:33 +03:00
parent 1376ff9188
commit d959dcca96
82 changed files with 25041 additions and 1 deletions

193
internal/ai/openai.go Normal file
View File

@@ -0,0 +1,193 @@
package ai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"smart-search-back/pkg/errors"
)
type OpenAIClient struct {
apiKey string
client *http.Client
}
type openAIRequest struct {
Model string `json:"model"`
Messages []openAIMessage `json:"messages"`
}
type openAIMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type openAIResponse struct {
Choices []struct {
Message openAIMessage `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
type tzResponse struct {
TZResponse string `json:"tz_response"`
}
func NewOpenAIClient(apiKey string) *OpenAIClient {
return &OpenAIClient{
apiKey: apiKey,
client: &http.Client{},
}
}
func (c *OpenAIClient) isMockMode() bool {
return c.apiKey == ""
}
func (c *OpenAIClient) GenerateTZ(requestTxt string) (string, error) {
if c.isMockMode() {
return c.generateMockTZ(requestTxt), nil
}
prompt := fmt.Sprintf(`Ты — эксперт по разработке технических заданий.
ЗАДАЧА:
Преобразовать описание заказа в структурированное техническое задание.
ВХОДНЫЕ ДАННЫЕ:
Описание: %s
СТРУКТУРА ТЗ:
1. Наименование и описание
2. Количество и единицы измерения
3. Технические требования
4. Сроки выполнения/поставки
6. Требования к качеству (если есть)
7. Стоимость/бюджет (если указан)
8. Особые условия
ПРИМЕР:
Входные данные: "Нужны офисные столы деревянные, 10 шт, белого цвета, на колесиках, 50 тысяч, на ул. Пушкина"
Ответ:
{
"tz_response": "ТЕХНИЧЕСКОЕ ЗАДАНИЕ\n\n1. ПРЕДМЕТ\nПоставка офисных столов\n\n2. КОЛИЧЕСТВО\n10 шт.\n\n3. ТРЕБОВАНИЯ\n- Материал: дерево\n- Цвет: белый\n- На колесиках\n\n4. МЕСТО ДОСТАВКИ\nг. Москва, ул. Пушкина\n\n5. БЮДЖЕТ\n50 000 руб.\n\n6. СРОКИ\nОт согласования"
}
ФОРМАТ ОТВЕТА:
{
"tz_response": "ТЕХНИЧЕСКОЕ ЗАДАНИЕ\n\n1. РАЗДЕЛ\nДетали...\n\n2. РАЗДЕЛ\nДетали..."
}
ПРАВИЛА:
- Ответ ТОЛЬКО в формате JSON
- Используй \n для переносов строк
- Без текста до или после JSON
- Ясная нумерация разделов
- Все параметры из описания должны быть в ТЗ
- Если параметр не указан → укажи "Не указано"`, requestTxt)
reqBody := openAIRequest{
Model: "gpt-4o-mini",
Messages: []openAIMessage{
{
Role: "user",
Content: prompt,
},
},
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", errors.NewInternalError(errors.AIAPIError, "failed to marshal request", err)
}
req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(jsonData))
if err != nil {
return "", errors.NewInternalError(errors.AIAPIError, "failed to create request", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.client.Do(req)
if err != nil {
return "", errors.NewInternalError(errors.AIAPIError, "failed to send request", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.NewInternalError(errors.AIAPIError, "failed to read response", err)
}
var aiResp openAIResponse
if err := json.Unmarshal(body, &aiResp); err != nil {
return "", errors.NewInternalError(errors.AIAPIError, "failed to unmarshal response", err)
}
if aiResp.Error != nil {
return "", errors.NewInternalError(errors.AIAPIError, aiResp.Error.Message, nil)
}
if len(aiResp.Choices) == 0 {
return "", errors.NewInternalError(errors.AIAPIError, "no choices in response", nil)
}
content := aiResp.Choices[0].Message.Content
content = strings.TrimPrefix(content, "```json")
content = strings.TrimPrefix(content, "```")
content = strings.TrimSuffix(content, "```")
content = strings.TrimSpace(content)
var tzResp tzResponse
if err := json.Unmarshal([]byte(content), &tzResp); err != nil {
re := regexp.MustCompile(`\{[\s\S]*\}`)
match := re.FindString(content)
if match != "" {
if err := json.Unmarshal([]byte(match), &tzResp); err != nil {
return "", errors.NewInternalError(errors.AIAPIError, "failed to parse TZ response", err)
}
} else {
return "", errors.NewInternalError(errors.AIAPIError, "failed to parse TZ response", err)
}
}
return tzResp.TZResponse, nil
}
func (c *OpenAIClient) generateMockTZ(requestTxt string) string {
return fmt.Sprintf(`ТЕХНИЧЕСКОЕ ЗАДАНИЕ (MOCK)
1. ПРЕДМЕТ
%s
2. КОЛИЧЕСТВО
По запросу
3. ТРЕБОВАНИЯ
- Качественное исполнение
- Соответствие стандартам
- Своевременная поставка
4. МЕСТО ДОСТАВКИ
г. Москва
5. БЮДЖЕТ
Уточняется
6. СРОКИ
От согласования
7. ОСОБЫЕ УСЛОВИЯ
Mock-данные для тестирования. API ключ OpenAI не настроен.`, requestTxt)
}

321
internal/ai/perplexity.go Normal file
View File

@@ -0,0 +1,321 @@
package ai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"smart-search-back/internal/model"
"smart-search-back/pkg/errors"
)
type PerplexityClient struct {
apiKey string
client *http.Client
}
type perplexityRequest struct {
Model string `json:"model"`
Messages []perplexityMessage `json:"messages"`
}
type perplexityMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type perplexityResponse struct {
Choices []struct {
Message perplexityMessage `json:"message"`
} `json:"choices"`
Usage *struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
} `json:"usage,omitempty"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
type supplierData struct {
CompanyName string `json:"company_name"`
Email string `json:"email"`
Phone string `json:"phone"`
Address string `json:"adress"`
URL string `json:"url"`
}
func NewPerplexityClient(apiKey string) *PerplexityClient {
return &PerplexityClient{
apiKey: apiKey,
client: &http.Client{},
}
}
func (c *PerplexityClient) isMockMode() bool {
return c.apiKey == ""
}
func (c *PerplexityClient) FindSuppliers(tzText string) ([]*model.Supplier, int, int, error) {
if c.isMockMode() {
return c.generateMockSuppliers(), 1000, 500, nil
}
prompt := fmt.Sprintf(`Ты — эксперт по поиску поставщиков на российском рынке.
ЗАДАЧА:
Найти компании-поставщики/производители для технического задания:
%s
ОПРЕДЕЛЕНИЕ ТИПА ПОИСКА:
- Если ТЗ требует производства/изготовления → ищи производителей
- Если ТЗ требует готовый товар/услугу → ищи компании, которые это продают
КРИТЕРИИ:
1. Максимальная релевантность
2. Действующие компании с полными контактами
3. Приоритет: производители > дистрибьюторы
ПРИМЕРЫ ОТВЕТОВ:
Пример 1 - ТЗ: "Поставка офисной мебели: столы 20 шт, стулья 50 шт"
[
{
"company_name": "ООО Мебельная фабрика Союз",
"email": "zakaz@mebelsoyuz.ru",
"phone": "+7 495 123-45-67",
"adress": "г. Москва, ул. Промышленная, д. 15",
"url": ""
}
]
Пример 2 - ТЗ: "Производство кованых изделий: ворота, решётки, перила"
[
{
"company_name": "ООО Кузня Премиум",
"email": "info@kuzniya.ru",
"phone": "",
"adress": "г. Москва, ул. Заводская, д. 42",
"url": "www.mebelsoyuz.ru"
}
]
ФОРМАТ ОТВЕТА - ТОЛЬКО JSON:
[
{
"company_name": "...",
"email": "...",
"phone": "...",
"adress": "...",
"url": "..."
}
]
ПРАВИЛА:
- Минимум 15 компаний, максимум 100
- Только валидный JSON массив, без текста вокруг
- email и url всегда заполнены (не null)
- Если другие поля пустые, то можно оставить пустую строку -> ""
- Сортируй по релевантности`, tzText)
reqBody := perplexityRequest{
Model: "llama-3.1-sonar-large-128k-online",
Messages: []perplexityMessage{
{
Role: "user",
Content: prompt,
},
},
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to marshal request", err)
}
req, err := http.NewRequest("POST", "https://api.perplexity.ai/chat/completions", bytes.NewBuffer(jsonData))
if err != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to create request", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.client.Do(req)
if err != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to send request", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to read response", err)
}
var aiResp perplexityResponse
if err := json.Unmarshal(body, &aiResp); err != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to unmarshal response", err)
}
if aiResp.Error != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, aiResp.Error.Message, nil)
}
if len(aiResp.Choices) == 0 {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "no choices in response", nil)
}
content := aiResp.Choices[0].Message.Content
content = strings.TrimPrefix(content, "```json")
content = strings.TrimPrefix(content, "```")
content = strings.TrimSuffix(content, "```")
content = strings.TrimSpace(content)
var supplierDataList []supplierData
if err := json.Unmarshal([]byte(content), &supplierDataList); err != nil {
re := regexp.MustCompile(`\[[\s\S]*\]`)
match := re.FindString(content)
if match != "" {
if err := json.Unmarshal([]byte(match), &supplierDataList); err != nil {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to parse suppliers response", err)
}
} else {
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to parse suppliers response", err)
}
}
suppliers := make([]*model.Supplier, 0, len(supplierDataList))
for _, sd := range supplierDataList {
suppliers = append(suppliers, &model.Supplier{
Name: sd.CompanyName,
Email: sd.Email,
Phone: sd.Phone,
Address: sd.Address,
URL: sd.URL,
})
}
promptTokens := 0
responseTokens := 0
if aiResp.Usage != nil {
promptTokens = aiResp.Usage.PromptTokens
responseTokens = aiResp.Usage.CompletionTokens
}
return suppliers, promptTokens, responseTokens, nil
}
func (c *PerplexityClient) generateMockSuppliers() []*model.Supplier {
return []*model.Supplier{
{
Name: "ООО Поставщик-1 (Mock)",
Email: "supplier1@example.com",
Phone: "+7 (495) 123-45-67",
Address: "г. Москва, ул. Примерная, д. 1",
URL: "https://supplier1.example.com",
},
{
Name: "ООО Поставщик-2 (Mock)",
Email: "supplier2@example.com",
Phone: "+7 (495) 234-56-78",
Address: "г. Москва, ул. Примерная, д. 2",
URL: "https://supplier2.example.com",
},
{
Name: "ООО Поставщик-3 (Mock)",
Email: "supplier3@example.com",
Phone: "+7 (495) 345-67-89",
Address: "г. Москва, ул. Примерная, д. 3",
URL: "https://supplier3.example.com",
},
{
Name: "ООО Производитель-1 (Mock)",
Email: "producer1@example.com",
Phone: "+7 (495) 456-78-90",
Address: "г. Санкт-Петербург, ул. Тестовая, д. 10",
URL: "https://producer1.example.com",
},
{
Name: "ООО Производитель-2 (Mock)",
Email: "producer2@example.com",
Phone: "+7 (495) 567-89-01",
Address: "г. Санкт-Петербург, ул. Тестовая, д. 20",
URL: "https://producer2.example.com",
},
{
Name: "ООО Дистрибьютор-1 (Mock)",
Email: "distributor1@example.com",
Phone: "+7 (495) 678-90-12",
Address: "г. Казань, ул. Демо, д. 5",
URL: "https://distributor1.example.com",
},
{
Name: "ООО Дистрибьютор-2 (Mock)",
Email: "distributor2@example.com",
Phone: "+7 (495) 789-01-23",
Address: "г. Казань, ул. Демо, д. 15",
URL: "https://distributor2.example.com",
},
{
Name: "ООО Импортер-1 (Mock)",
Email: "importer1@example.com",
Phone: "+7 (495) 890-12-34",
Address: "г. Новосибирск, ул. Примера, д. 100",
URL: "https://importer1.example.com",
},
{
Name: "ООО Импортер-2 (Mock)",
Email: "importer2@example.com",
Phone: "+7 (495) 901-23-45",
Address: "г. Новосибирск, ул. Примера, д. 200",
URL: "https://importer2.example.com",
},
{
Name: "ООО Оптовик-1 (Mock)",
Email: "wholesale1@example.com",
Phone: "+7 (495) 012-34-56",
Address: "г. Екатеринбург, ул. Тестовая, д. 50",
URL: "https://wholesale1.example.com",
},
{
Name: "ООО Оптовик-2 (Mock)",
Email: "wholesale2@example.com",
Phone: "+7 (495) 123-45-67",
Address: "г. Екатеринбург, ул. Тестовая, д. 60",
URL: "https://wholesale2.example.com",
},
{
Name: "ООО Фабрика-1 (Mock)",
Email: "factory1@example.com",
Phone: "+7 (495) 234-56-78",
Address: "г. Нижний Новгород, Промзона, д. 1",
URL: "https://factory1.example.com",
},
{
Name: "ООО Фабрика-2 (Mock)",
Email: "factory2@example.com",
Phone: "+7 (495) 345-67-89",
Address: "г. Нижний Новгород, Промзона, д. 2",
URL: "https://factory2.example.com",
},
{
Name: "ООО Завод-1 (Mock)",
Email: "plant1@example.com",
Phone: "+7 (495) 456-78-90",
Address: "г. Челябинск, Индустриальная, д. 10",
URL: "https://plant1.example.com",
},
{
Name: "ООО Завод-2 (Mock)",
Email: "plant2@example.com",
Phone: "+7 (495) 567-89-01",
Address: "г. Челябинск, Индустриальная, д. 20",
URL: "https://plant2.example.com",
},
}
}

111
internal/config/config.go Normal file
View File

@@ -0,0 +1,111 @@
package config
import (
"fmt"
"os"
"regexp"
"gopkg.in/yaml.v3"
)
type Config struct {
Database DatabaseConfig `yaml:"database"`
AI AIConfig `yaml:"ai"`
Security SecurityConfig `yaml:"security"`
GRPC GRPCConfig `yaml:"grpc"`
Logging LoggingConfig `yaml:"logging"`
}
type DatabaseConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Name string `yaml:"name"`
User string `yaml:"user"`
Password string `yaml:"password"`
SSLMode string `yaml:"ssl_mode"`
MaxConns int `yaml:"max_conns"`
MinConns int `yaml:"min_conns"`
}
type AIConfig struct {
OpenAIKey string `yaml:"openai_key"`
PerplexityKey string `yaml:"perplexity_key"`
}
type SecurityConfig struct {
JWTSecret string `yaml:"jwt_secret"`
CryptoSecret string `yaml:"crypto_secret"`
}
type GRPCConfig struct {
Port int `yaml:"port"`
MaxConnections int `yaml:"max_connections"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
data = []byte(expandEnvWithDefaults(string(data)))
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &cfg, nil
}
func expandEnvWithDefaults(s string) string {
re := regexp.MustCompile(`\$\{([^:}]+):([^}]*)\}`)
result := re.ReplaceAllStringFunc(s, func(match string) string {
parts := re.FindStringSubmatch(match)
if len(parts) != 3 {
return match
}
envVar := parts[1]
defaultVal := parts[2]
if val := os.Getenv(envVar); val != "" {
return val
}
return defaultVal
})
return os.ExpandEnv(result)
}
func (c *Config) validate() error {
if c.Database.Host == "" {
return fmt.Errorf("database host is required")
}
if c.Database.Name == "" {
return fmt.Errorf("database name is required")
}
if c.Database.User == "" {
return fmt.Errorf("database user is required")
}
if c.Database.Password == "" {
return fmt.Errorf("database password is required")
}
if c.Security.JWTSecret == "" {
return fmt.Errorf("JWT secret is required")
}
if c.Security.CryptoSecret == "" {
return fmt.Errorf("crypto secret is required")
}
return nil
}
func (c *Config) DatabaseURL() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
c.Database.User, c.Database.Password, c.Database.Host, c.Database.Port, c.Database.Name, c.Database.SSLMode)
}

View File

@@ -0,0 +1,31 @@
package database
import (
"database/sql"
"fmt"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)
func RunMigrations(databaseURL string) error {
db, err := sql.Open("pgx", databaseURL)
if err != nil {
return fmt.Errorf("failed to open database connection for migrations: %w", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
return fmt.Errorf("failed to ping database before migrations: %w", err)
}
if err := goose.SetDialect("postgres"); err != nil {
return fmt.Errorf("failed to set goose dialect: %w", err)
}
if err := goose.Up(db, "migrations"); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
return nil
}

View File

@@ -0,0 +1,70 @@
package grpc
import (
"context"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/auth"
"go.uber.org/zap"
)
func (h *AuthHandler) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
accessToken, refreshToken, err := h.authService.Login(
ctx,
req.Email,
req.Password,
req.Ip,
req.UserAgent,
)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "AuthService.Login")
}
return &pb.LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}
func (h *AuthHandler) Refresh(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
accessToken, err := h.authService.Refresh(ctx, req.RefreshToken)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "AuthService.Refresh")
}
return &pb.RefreshResponse{
AccessToken: accessToken,
RefreshToken: req.RefreshToken,
}, nil
}
func (h *AuthHandler) Validate(ctx context.Context, req *pb.ValidateRequest) (*pb.ValidateResponse, error) {
userID, err := h.authService.Validate(ctx, req.AccessToken)
if err != nil {
h.logger.Warn("Token validation failed",
zap.String("method", "AuthService.Validate"),
zap.Error(err),
)
return &pb.ValidateResponse{
Valid: false,
UserId: 0,
}, nil
}
return &pb.ValidateResponse{
Valid: true,
UserId: int64(userID),
}, nil
}
func (h *AuthHandler) Logout(ctx context.Context, req *pb.LogoutRequest) (*pb.LogoutResponse, error) {
err := h.authService.Logout(ctx, req.AccessToken)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "AuthService.Logout")
}
return &pb.LogoutResponse{
Success: true,
}, nil
}

View File

@@ -0,0 +1,45 @@
package grpc
import (
"context"
"strconv"
"google.golang.org/protobuf/types/known/timestamppb"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/invite"
)
func (h *InviteHandler) Generate(ctx context.Context, req *pb.GenerateRequest) (*pb.GenerateResponse, error) {
invite, err := h.inviteService.Generate(ctx, int(req.UserId), int(req.TtlDays), int(req.MaxUses))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "InviteService.Generate")
}
return &pb.GenerateResponse{
Code: strconv.FormatInt(invite.Code, 10),
MaxUses: int32(invite.CanBeUsedCount),
ExpiresAt: timestamppb.New(invite.ExpiresAt),
}, nil
}
func (h *InviteHandler) GetInfo(ctx context.Context, req *pb.GetInfoRequest) (*pb.GetInfoResponse, error) {
code, err := strconv.ParseInt(req.Code, 10, 64)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "InviteService.GetInfo")
}
invite, err := h.inviteService.GetInfo(ctx, code)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "InviteService.GetInfo")
}
return &pb.GetInfoResponse{
Code: strconv.FormatInt(invite.Code, 10),
UserId: int64(invite.UserID),
CanBeUsedCount: int32(invite.CanBeUsedCount),
UsedCount: int32(invite.UsedCount),
ExpiresAt: timestamppb.New(invite.ExpiresAt),
IsActive: invite.IsActive,
CreatedAt: timestamppb.New(invite.CreatedAt),
}, nil
}

View File

@@ -0,0 +1,91 @@
package grpc
import (
"context"
"time"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/request"
)
func (h *RequestHandler) CreateTZ(ctx context.Context, req *pb.CreateTZRequest) (*pb.CreateTZResponse, error) {
requestTxt := req.RequestTxt
if len(req.FileData) > 0 {
requestTxt += "\n[File: " + req.FileName + "]"
}
requestID, tzText, err := h.requestService.CreateTZ(ctx, int(req.UserId), requestTxt)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "RequestService.CreateTZ")
}
return &pb.CreateTZResponse{
RequestId: requestID.String(),
TzText: tzText,
}, nil
}
func (h *RequestHandler) ApproveTZ(ctx context.Context, req *pb.ApproveTZRequest) (*pb.ApproveTZResponse, error) {
requestID, err := uuid.Parse(req.RequestId)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "RequestService.ApproveTZ")
}
_, err = h.requestService.ApproveTZ(ctx, requestID, req.FinalTz, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "RequestService.ApproveTZ")
}
return &pb.ApproveTZResponse{
Success: true,
MailingStatus: "sent",
}, nil
}
func (h *RequestHandler) GetMailingList(ctx context.Context, req *pb.GetMailingListRequest) (*pb.GetMailingListResponse, error) {
requests, err := h.requestService.GetMailingList(ctx, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "RequestService.GetMailingList")
}
items := make([]*pb.MailingItem, 0, len(requests))
for _, r := range requests {
items = append(items, &pb.MailingItem{
RequestId: r.ID.String(),
RequestTxt: r.RequestTxt,
FinalTz: r.FinalTZ,
MailingStatus: r.MailingStatus,
CreatedAt: timestamppb.New(r.CreatedAt),
SuppliersFound: 0,
})
}
return &pb.GetMailingListResponse{
Items: items,
}, nil
}
func (h *RequestHandler) GetMailingListByID(ctx context.Context, req *pb.GetMailingListByIDRequest) (*pb.GetMailingListByIDResponse, error) {
requestID, err := uuid.Parse(req.RequestId)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "RequestService.GetMailingListByID")
}
detail, err := h.requestService.GetMailingListByID(ctx, requestID)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "RequestService.GetMailingListByID")
}
return &pb.GetMailingListByIDResponse{
Item: &pb.MailingItem{
RequestId: detail.RequestID.String(),
RequestTxt: detail.Title,
FinalTz: detail.MailText,
MailingStatus: "sent",
CreatedAt: timestamppb.New(time.Now()),
SuppliersFound: int32(len(detail.Suppliers)),
},
}, nil
}

78
internal/grpc/server.go Normal file
View File

@@ -0,0 +1,78 @@
package grpc
import (
"smart-search-back/internal/ai"
"smart-search-back/internal/repository"
"smart-search-back/internal/service"
authpb "smart-search-back/pkg/pb/api/proto/auth"
invitepb "smart-search-back/pkg/pb/api/proto/invite"
requestpb "smart-search-back/pkg/pb/api/proto/request"
supplierpb "smart-search-back/pkg/pb/api/proto/supplier"
userpb "smart-search-back/pkg/pb/api/proto/user"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
"google.golang.org/grpc"
)
type AuthHandler struct {
authpb.UnimplementedAuthServiceServer
authService service.AuthService
logger *zap.Logger
}
type UserHandler struct {
userpb.UnimplementedUserServiceServer
userService service.UserService
logger *zap.Logger
}
type InviteHandler struct {
invitepb.UnimplementedInviteServiceServer
inviteService service.InviteService
logger *zap.Logger
}
type RequestHandler struct {
requestpb.UnimplementedRequestServiceServer
requestService service.RequestService
logger *zap.Logger
}
type SupplierHandler struct {
supplierpb.UnimplementedSupplierServiceServer
supplierService service.SupplierService
logger *zap.Logger
}
func NewHandlers(pool *pgxpool.Pool, jwtSecret, cryptoSecret, openAIKey, perplexityKey string, logger *zap.Logger) (*AuthHandler, *UserHandler, *InviteHandler, *RequestHandler, *SupplierHandler) {
userRepo := repository.NewUserRepository(pool, cryptoSecret)
sessionRepo := repository.NewSessionRepository(pool)
inviteRepo := repository.NewInviteRepository(pool)
requestRepo := repository.NewRequestRepository(pool)
supplierRepo := repository.NewSupplierRepository(pool)
tokenUsageRepo := repository.NewTokenUsageRepository(pool)
openAIClient := ai.NewOpenAIClient(openAIKey)
perplexityClient := ai.NewPerplexityClient(perplexityKey)
authService := service.NewAuthService(userRepo, sessionRepo, jwtSecret, cryptoSecret)
userService := service.NewUserService(userRepo, requestRepo, cryptoSecret)
inviteService := service.NewInviteService(inviteRepo, userRepo)
requestService := service.NewRequestService(requestRepo, supplierRepo, tokenUsageRepo, userRepo, openAIClient, perplexityClient)
supplierService := service.NewSupplierService(supplierRepo)
return &AuthHandler{authService: authService, logger: logger},
&UserHandler{userService: userService, logger: logger},
&InviteHandler{inviteService: inviteService, logger: logger},
&RequestHandler{requestService: requestService, logger: logger},
&SupplierHandler{supplierService: supplierService, logger: logger}
}
func RegisterServices(s *grpc.Server, authH *AuthHandler, userH *UserHandler, inviteH *InviteHandler, requestH *RequestHandler, supplierH *SupplierHandler) {
authpb.RegisterAuthServiceServer(s, authH)
userpb.RegisterUserServiceServer(s, userH)
invitepb.RegisterInviteServiceServer(s, inviteH)
requestpb.RegisterRequestServiceServer(s, requestH)
supplierpb.RegisterSupplierServiceServer(s, supplierH)
}

View File

@@ -0,0 +1,29 @@
package grpc
import (
"context"
"github.com/google/uuid"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/supplier"
)
func (h *SupplierHandler) ExportExcel(ctx context.Context, req *pb.ExportExcelRequest) (*pb.ExportExcelResponse, error) {
requestID, err := uuid.Parse(req.RequestId)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "SupplierService.ExportExcel")
}
fileData, err := h.supplierService.ExportExcel(ctx, requestID)
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "SupplierService.ExportExcel")
}
fileName := "suppliers_" + requestID.String() + ".xlsx"
return &pb.ExportExcelResponse{
FileData: fileData,
FileName: fileName,
MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}, nil
}

View File

@@ -0,0 +1,66 @@
package grpc
import (
"context"
"smart-search-back/pkg/errors"
pb "smart-search-back/pkg/pb/api/proto/user"
)
func (h *UserHandler) GetInfo(ctx context.Context, req *pb.GetInfoRequest) (*pb.GetInfoResponse, error) {
user, err := h.userService.GetInfo(ctx, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "UserService.GetInfo")
}
return &pb.GetInfoResponse{
Email: user.Email,
Name: user.Name,
Phone: user.Phone,
CompanyName: user.CompanyName,
PaymentStatus: user.PaymentStatus,
}, nil
}
func (h *UserHandler) GetBalance(ctx context.Context, req *pb.GetBalanceRequest) (*pb.GetBalanceResponse, error) {
balance, err := h.userService.GetBalance(ctx, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "UserService.GetBalance")
}
return &pb.GetBalanceResponse{
Balance: balance,
}, nil
}
func (h *UserHandler) GetStatistics(ctx context.Context, req *pb.GetStatisticsRequest) (*pb.GetStatisticsResponse, error) {
stats, err := h.userService.GetStatistics(ctx, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "UserService.GetStatistics")
}
return &pb.GetStatisticsResponse{
TotalRequests: int32(stats.RequestsCount),
SuccessfulRequests: int32(stats.SuppliersCount),
FailedRequests: 0,
TotalSpent: 0,
}, nil
}
func (h *UserHandler) GetBalanceStatistics(ctx context.Context, req *pb.GetBalanceStatisticsRequest) (*pb.GetBalanceStatisticsResponse, error) {
balance, err := h.userService.GetBalance(ctx, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "UserService.GetBalanceStatistics")
}
stats, err := h.userService.GetStatistics(ctx, int(req.UserId))
if err != nil {
return nil, errors.ToGRPCError(err, h.logger, "UserService.GetBalanceStatistics")
}
return &pb.GetBalanceStatisticsResponse{
Balance: balance,
TotalRequests: int32(stats.RequestsCount),
TotalSpent: 0,
}, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,836 @@
// Code generated by http://github.com/gojuno/minimock (v3.4.7). DO NOT EDIT.
package mocks
//go:generate minimock -i smart-search-back/internal/service.InviteService -o invite_service_mock.go -n InviteServiceMock -p mocks
import (
"context"
"smart-search-back/internal/model"
"sync"
mm_atomic "sync/atomic"
mm_time "time"
"github.com/gojuno/minimock/v3"
)
// InviteServiceMock implements mm_service.InviteService
type InviteServiceMock struct {
t minimock.Tester
finishOnce sync.Once
funcGenerate func(ctx context.Context, userID int, maxUses int, ttlDays int) (ip1 *model.InviteCode, err error)
funcGenerateOrigin string
inspectFuncGenerate func(ctx context.Context, userID int, maxUses int, ttlDays int)
afterGenerateCounter uint64
beforeGenerateCounter uint64
GenerateMock mInviteServiceMockGenerate
funcGetInfo func(ctx context.Context, code int64) (ip1 *model.InviteCode, err error)
funcGetInfoOrigin string
inspectFuncGetInfo func(ctx context.Context, code int64)
afterGetInfoCounter uint64
beforeGetInfoCounter uint64
GetInfoMock mInviteServiceMockGetInfo
}
// NewInviteServiceMock returns a mock for mm_service.InviteService
func NewInviteServiceMock(t minimock.Tester) *InviteServiceMock {
m := &InviteServiceMock{t: t}
if controller, ok := t.(minimock.MockController); ok {
controller.RegisterMocker(m)
}
m.GenerateMock = mInviteServiceMockGenerate{mock: m}
m.GenerateMock.callArgs = []*InviteServiceMockGenerateParams{}
m.GetInfoMock = mInviteServiceMockGetInfo{mock: m}
m.GetInfoMock.callArgs = []*InviteServiceMockGetInfoParams{}
t.Cleanup(m.MinimockFinish)
return m
}
type mInviteServiceMockGenerate struct {
optional bool
mock *InviteServiceMock
defaultExpectation *InviteServiceMockGenerateExpectation
expectations []*InviteServiceMockGenerateExpectation
callArgs []*InviteServiceMockGenerateParams
mutex sync.RWMutex
expectedInvocations uint64
expectedInvocationsOrigin string
}
// InviteServiceMockGenerateExpectation specifies expectation struct of the InviteService.Generate
type InviteServiceMockGenerateExpectation struct {
mock *InviteServiceMock
params *InviteServiceMockGenerateParams
paramPtrs *InviteServiceMockGenerateParamPtrs
expectationOrigins InviteServiceMockGenerateExpectationOrigins
results *InviteServiceMockGenerateResults
returnOrigin string
Counter uint64
}
// InviteServiceMockGenerateParams contains parameters of the InviteService.Generate
type InviteServiceMockGenerateParams struct {
ctx context.Context
userID int
maxUses int
ttlDays int
}
// InviteServiceMockGenerateParamPtrs contains pointers to parameters of the InviteService.Generate
type InviteServiceMockGenerateParamPtrs struct {
ctx *context.Context
userID *int
maxUses *int
ttlDays *int
}
// InviteServiceMockGenerateResults contains results of the InviteService.Generate
type InviteServiceMockGenerateResults struct {
ip1 *model.InviteCode
err error
}
// InviteServiceMockGenerateOrigins contains origins of expectations of the InviteService.Generate
type InviteServiceMockGenerateExpectationOrigins struct {
origin string
originCtx string
originUserID string
originMaxUses string
originTtlDays string
}
// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning
// the test will fail minimock's automatic final call check if the mocked method was not called at least once.
// Optional() makes method check to work in '0 or more' mode.
// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to
// catch the problems when the expected method call is totally skipped during test run.
func (mmGenerate *mInviteServiceMockGenerate) Optional() *mInviteServiceMockGenerate {
mmGenerate.optional = true
return mmGenerate
}
// Expect sets up expected params for InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) Expect(ctx context.Context, userID int, maxUses int, ttlDays int) *mInviteServiceMockGenerate {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
if mmGenerate.defaultExpectation == nil {
mmGenerate.defaultExpectation = &InviteServiceMockGenerateExpectation{}
}
if mmGenerate.defaultExpectation.paramPtrs != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by ExpectParams functions")
}
mmGenerate.defaultExpectation.params = &InviteServiceMockGenerateParams{ctx, userID, maxUses, ttlDays}
mmGenerate.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1)
for _, e := range mmGenerate.expectations {
if minimock.Equal(e.params, mmGenerate.defaultExpectation.params) {
mmGenerate.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGenerate.defaultExpectation.params)
}
}
return mmGenerate
}
// ExpectCtxParam1 sets up expected param ctx for InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) ExpectCtxParam1(ctx context.Context) *mInviteServiceMockGenerate {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
if mmGenerate.defaultExpectation == nil {
mmGenerate.defaultExpectation = &InviteServiceMockGenerateExpectation{}
}
if mmGenerate.defaultExpectation.params != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Expect")
}
if mmGenerate.defaultExpectation.paramPtrs == nil {
mmGenerate.defaultExpectation.paramPtrs = &InviteServiceMockGenerateParamPtrs{}
}
mmGenerate.defaultExpectation.paramPtrs.ctx = &ctx
mmGenerate.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1)
return mmGenerate
}
// ExpectUserIDParam2 sets up expected param userID for InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) ExpectUserIDParam2(userID int) *mInviteServiceMockGenerate {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
if mmGenerate.defaultExpectation == nil {
mmGenerate.defaultExpectation = &InviteServiceMockGenerateExpectation{}
}
if mmGenerate.defaultExpectation.params != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Expect")
}
if mmGenerate.defaultExpectation.paramPtrs == nil {
mmGenerate.defaultExpectation.paramPtrs = &InviteServiceMockGenerateParamPtrs{}
}
mmGenerate.defaultExpectation.paramPtrs.userID = &userID
mmGenerate.defaultExpectation.expectationOrigins.originUserID = minimock.CallerInfo(1)
return mmGenerate
}
// ExpectMaxUsesParam3 sets up expected param maxUses for InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) ExpectMaxUsesParam3(maxUses int) *mInviteServiceMockGenerate {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
if mmGenerate.defaultExpectation == nil {
mmGenerate.defaultExpectation = &InviteServiceMockGenerateExpectation{}
}
if mmGenerate.defaultExpectation.params != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Expect")
}
if mmGenerate.defaultExpectation.paramPtrs == nil {
mmGenerate.defaultExpectation.paramPtrs = &InviteServiceMockGenerateParamPtrs{}
}
mmGenerate.defaultExpectation.paramPtrs.maxUses = &maxUses
mmGenerate.defaultExpectation.expectationOrigins.originMaxUses = minimock.CallerInfo(1)
return mmGenerate
}
// ExpectTtlDaysParam4 sets up expected param ttlDays for InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) ExpectTtlDaysParam4(ttlDays int) *mInviteServiceMockGenerate {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
if mmGenerate.defaultExpectation == nil {
mmGenerate.defaultExpectation = &InviteServiceMockGenerateExpectation{}
}
if mmGenerate.defaultExpectation.params != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Expect")
}
if mmGenerate.defaultExpectation.paramPtrs == nil {
mmGenerate.defaultExpectation.paramPtrs = &InviteServiceMockGenerateParamPtrs{}
}
mmGenerate.defaultExpectation.paramPtrs.ttlDays = &ttlDays
mmGenerate.defaultExpectation.expectationOrigins.originTtlDays = minimock.CallerInfo(1)
return mmGenerate
}
// Inspect accepts an inspector function that has same arguments as the InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) Inspect(f func(ctx context.Context, userID int, maxUses int, ttlDays int)) *mInviteServiceMockGenerate {
if mmGenerate.mock.inspectFuncGenerate != nil {
mmGenerate.mock.t.Fatalf("Inspect function is already set for InviteServiceMock.Generate")
}
mmGenerate.mock.inspectFuncGenerate = f
return mmGenerate
}
// Return sets up results that will be returned by InviteService.Generate
func (mmGenerate *mInviteServiceMockGenerate) Return(ip1 *model.InviteCode, err error) *InviteServiceMock {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
if mmGenerate.defaultExpectation == nil {
mmGenerate.defaultExpectation = &InviteServiceMockGenerateExpectation{mock: mmGenerate.mock}
}
mmGenerate.defaultExpectation.results = &InviteServiceMockGenerateResults{ip1, err}
mmGenerate.defaultExpectation.returnOrigin = minimock.CallerInfo(1)
return mmGenerate.mock
}
// Set uses given function f to mock the InviteService.Generate method
func (mmGenerate *mInviteServiceMockGenerate) Set(f func(ctx context.Context, userID int, maxUses int, ttlDays int) (ip1 *model.InviteCode, err error)) *InviteServiceMock {
if mmGenerate.defaultExpectation != nil {
mmGenerate.mock.t.Fatalf("Default expectation is already set for the InviteService.Generate method")
}
if len(mmGenerate.expectations) > 0 {
mmGenerate.mock.t.Fatalf("Some expectations are already set for the InviteService.Generate method")
}
mmGenerate.mock.funcGenerate = f
mmGenerate.mock.funcGenerateOrigin = minimock.CallerInfo(1)
return mmGenerate.mock
}
// When sets expectation for the InviteService.Generate which will trigger the result defined by the following
// Then helper
func (mmGenerate *mInviteServiceMockGenerate) When(ctx context.Context, userID int, maxUses int, ttlDays int) *InviteServiceMockGenerateExpectation {
if mmGenerate.mock.funcGenerate != nil {
mmGenerate.mock.t.Fatalf("InviteServiceMock.Generate mock is already set by Set")
}
expectation := &InviteServiceMockGenerateExpectation{
mock: mmGenerate.mock,
params: &InviteServiceMockGenerateParams{ctx, userID, maxUses, ttlDays},
expectationOrigins: InviteServiceMockGenerateExpectationOrigins{origin: minimock.CallerInfo(1)},
}
mmGenerate.expectations = append(mmGenerate.expectations, expectation)
return expectation
}
// Then sets up InviteService.Generate return parameters for the expectation previously defined by the When method
func (e *InviteServiceMockGenerateExpectation) Then(ip1 *model.InviteCode, err error) *InviteServiceMock {
e.results = &InviteServiceMockGenerateResults{ip1, err}
return e.mock
}
// Times sets number of times InviteService.Generate should be invoked
func (mmGenerate *mInviteServiceMockGenerate) Times(n uint64) *mInviteServiceMockGenerate {
if n == 0 {
mmGenerate.mock.t.Fatalf("Times of InviteServiceMock.Generate mock can not be zero")
}
mm_atomic.StoreUint64(&mmGenerate.expectedInvocations, n)
mmGenerate.expectedInvocationsOrigin = minimock.CallerInfo(1)
return mmGenerate
}
func (mmGenerate *mInviteServiceMockGenerate) invocationsDone() bool {
if len(mmGenerate.expectations) == 0 && mmGenerate.defaultExpectation == nil && mmGenerate.mock.funcGenerate == nil {
return true
}
totalInvocations := mm_atomic.LoadUint64(&mmGenerate.mock.afterGenerateCounter)
expectedInvocations := mm_atomic.LoadUint64(&mmGenerate.expectedInvocations)
return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations)
}
// Generate implements mm_service.InviteService
func (mmGenerate *InviteServiceMock) Generate(ctx context.Context, userID int, maxUses int, ttlDays int) (ip1 *model.InviteCode, err error) {
mm_atomic.AddUint64(&mmGenerate.beforeGenerateCounter, 1)
defer mm_atomic.AddUint64(&mmGenerate.afterGenerateCounter, 1)
mmGenerate.t.Helper()
if mmGenerate.inspectFuncGenerate != nil {
mmGenerate.inspectFuncGenerate(ctx, userID, maxUses, ttlDays)
}
mm_params := InviteServiceMockGenerateParams{ctx, userID, maxUses, ttlDays}
// Record call args
mmGenerate.GenerateMock.mutex.Lock()
mmGenerate.GenerateMock.callArgs = append(mmGenerate.GenerateMock.callArgs, &mm_params)
mmGenerate.GenerateMock.mutex.Unlock()
for _, e := range mmGenerate.GenerateMock.expectations {
if minimock.Equal(*e.params, mm_params) {
mm_atomic.AddUint64(&e.Counter, 1)
return e.results.ip1, e.results.err
}
}
if mmGenerate.GenerateMock.defaultExpectation != nil {
mm_atomic.AddUint64(&mmGenerate.GenerateMock.defaultExpectation.Counter, 1)
mm_want := mmGenerate.GenerateMock.defaultExpectation.params
mm_want_ptrs := mmGenerate.GenerateMock.defaultExpectation.paramPtrs
mm_got := InviteServiceMockGenerateParams{ctx, userID, maxUses, ttlDays}
if mm_want_ptrs != nil {
if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) {
mmGenerate.t.Errorf("InviteServiceMock.Generate got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGenerate.GenerateMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx))
}
if mm_want_ptrs.userID != nil && !minimock.Equal(*mm_want_ptrs.userID, mm_got.userID) {
mmGenerate.t.Errorf("InviteServiceMock.Generate got unexpected parameter userID, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGenerate.GenerateMock.defaultExpectation.expectationOrigins.originUserID, *mm_want_ptrs.userID, mm_got.userID, minimock.Diff(*mm_want_ptrs.userID, mm_got.userID))
}
if mm_want_ptrs.maxUses != nil && !minimock.Equal(*mm_want_ptrs.maxUses, mm_got.maxUses) {
mmGenerate.t.Errorf("InviteServiceMock.Generate got unexpected parameter maxUses, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGenerate.GenerateMock.defaultExpectation.expectationOrigins.originMaxUses, *mm_want_ptrs.maxUses, mm_got.maxUses, minimock.Diff(*mm_want_ptrs.maxUses, mm_got.maxUses))
}
if mm_want_ptrs.ttlDays != nil && !minimock.Equal(*mm_want_ptrs.ttlDays, mm_got.ttlDays) {
mmGenerate.t.Errorf("InviteServiceMock.Generate got unexpected parameter ttlDays, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGenerate.GenerateMock.defaultExpectation.expectationOrigins.originTtlDays, *mm_want_ptrs.ttlDays, mm_got.ttlDays, minimock.Diff(*mm_want_ptrs.ttlDays, mm_got.ttlDays))
}
} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
mmGenerate.t.Errorf("InviteServiceMock.Generate got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGenerate.GenerateMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
}
mm_results := mmGenerate.GenerateMock.defaultExpectation.results
if mm_results == nil {
mmGenerate.t.Fatal("No results are set for the InviteServiceMock.Generate")
}
return (*mm_results).ip1, (*mm_results).err
}
if mmGenerate.funcGenerate != nil {
return mmGenerate.funcGenerate(ctx, userID, maxUses, ttlDays)
}
mmGenerate.t.Fatalf("Unexpected call to InviteServiceMock.Generate. %v %v %v %v", ctx, userID, maxUses, ttlDays)
return
}
// GenerateAfterCounter returns a count of finished InviteServiceMock.Generate invocations
func (mmGenerate *InviteServiceMock) GenerateAfterCounter() uint64 {
return mm_atomic.LoadUint64(&mmGenerate.afterGenerateCounter)
}
// GenerateBeforeCounter returns a count of InviteServiceMock.Generate invocations
func (mmGenerate *InviteServiceMock) GenerateBeforeCounter() uint64 {
return mm_atomic.LoadUint64(&mmGenerate.beforeGenerateCounter)
}
// Calls returns a list of arguments used in each call to InviteServiceMock.Generate.
// The list is in the same order as the calls were made (i.e. recent calls have a higher index)
func (mmGenerate *mInviteServiceMockGenerate) Calls() []*InviteServiceMockGenerateParams {
mmGenerate.mutex.RLock()
argCopy := make([]*InviteServiceMockGenerateParams, len(mmGenerate.callArgs))
copy(argCopy, mmGenerate.callArgs)
mmGenerate.mutex.RUnlock()
return argCopy
}
// MinimockGenerateDone returns true if the count of the Generate invocations corresponds
// the number of defined expectations
func (m *InviteServiceMock) MinimockGenerateDone() bool {
if m.GenerateMock.optional {
// Optional methods provide '0 or more' call count restriction.
return true
}
for _, e := range m.GenerateMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
return false
}
}
return m.GenerateMock.invocationsDone()
}
// MinimockGenerateInspect logs each unmet expectation
func (m *InviteServiceMock) MinimockGenerateInspect() {
for _, e := range m.GenerateMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
m.t.Errorf("Expected call to InviteServiceMock.Generate at\n%s with params: %#v", e.expectationOrigins.origin, *e.params)
}
}
afterGenerateCounter := mm_atomic.LoadUint64(&m.afterGenerateCounter)
// if default expectation was set then invocations count should be greater than zero
if m.GenerateMock.defaultExpectation != nil && afterGenerateCounter < 1 {
if m.GenerateMock.defaultExpectation.params == nil {
m.t.Errorf("Expected call to InviteServiceMock.Generate at\n%s", m.GenerateMock.defaultExpectation.returnOrigin)
} else {
m.t.Errorf("Expected call to InviteServiceMock.Generate at\n%s with params: %#v", m.GenerateMock.defaultExpectation.expectationOrigins.origin, *m.GenerateMock.defaultExpectation.params)
}
}
// if func was set then invocations count should be greater than zero
if m.funcGenerate != nil && afterGenerateCounter < 1 {
m.t.Errorf("Expected call to InviteServiceMock.Generate at\n%s", m.funcGenerateOrigin)
}
if !m.GenerateMock.invocationsDone() && afterGenerateCounter > 0 {
m.t.Errorf("Expected %d calls to InviteServiceMock.Generate at\n%s but found %d calls",
mm_atomic.LoadUint64(&m.GenerateMock.expectedInvocations), m.GenerateMock.expectedInvocationsOrigin, afterGenerateCounter)
}
}
type mInviteServiceMockGetInfo struct {
optional bool
mock *InviteServiceMock
defaultExpectation *InviteServiceMockGetInfoExpectation
expectations []*InviteServiceMockGetInfoExpectation
callArgs []*InviteServiceMockGetInfoParams
mutex sync.RWMutex
expectedInvocations uint64
expectedInvocationsOrigin string
}
// InviteServiceMockGetInfoExpectation specifies expectation struct of the InviteService.GetInfo
type InviteServiceMockGetInfoExpectation struct {
mock *InviteServiceMock
params *InviteServiceMockGetInfoParams
paramPtrs *InviteServiceMockGetInfoParamPtrs
expectationOrigins InviteServiceMockGetInfoExpectationOrigins
results *InviteServiceMockGetInfoResults
returnOrigin string
Counter uint64
}
// InviteServiceMockGetInfoParams contains parameters of the InviteService.GetInfo
type InviteServiceMockGetInfoParams struct {
ctx context.Context
code int64
}
// InviteServiceMockGetInfoParamPtrs contains pointers to parameters of the InviteService.GetInfo
type InviteServiceMockGetInfoParamPtrs struct {
ctx *context.Context
code *int64
}
// InviteServiceMockGetInfoResults contains results of the InviteService.GetInfo
type InviteServiceMockGetInfoResults struct {
ip1 *model.InviteCode
err error
}
// InviteServiceMockGetInfoOrigins contains origins of expectations of the InviteService.GetInfo
type InviteServiceMockGetInfoExpectationOrigins struct {
origin string
originCtx string
originCode string
}
// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning
// the test will fail minimock's automatic final call check if the mocked method was not called at least once.
// Optional() makes method check to work in '0 or more' mode.
// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to
// catch the problems when the expected method call is totally skipped during test run.
func (mmGetInfo *mInviteServiceMockGetInfo) Optional() *mInviteServiceMockGetInfo {
mmGetInfo.optional = true
return mmGetInfo
}
// Expect sets up expected params for InviteService.GetInfo
func (mmGetInfo *mInviteServiceMockGetInfo) Expect(ctx context.Context, code int64) *mInviteServiceMockGetInfo {
if mmGetInfo.mock.funcGetInfo != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Set")
}
if mmGetInfo.defaultExpectation == nil {
mmGetInfo.defaultExpectation = &InviteServiceMockGetInfoExpectation{}
}
if mmGetInfo.defaultExpectation.paramPtrs != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by ExpectParams functions")
}
mmGetInfo.defaultExpectation.params = &InviteServiceMockGetInfoParams{ctx, code}
mmGetInfo.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1)
for _, e := range mmGetInfo.expectations {
if minimock.Equal(e.params, mmGetInfo.defaultExpectation.params) {
mmGetInfo.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetInfo.defaultExpectation.params)
}
}
return mmGetInfo
}
// ExpectCtxParam1 sets up expected param ctx for InviteService.GetInfo
func (mmGetInfo *mInviteServiceMockGetInfo) ExpectCtxParam1(ctx context.Context) *mInviteServiceMockGetInfo {
if mmGetInfo.mock.funcGetInfo != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Set")
}
if mmGetInfo.defaultExpectation == nil {
mmGetInfo.defaultExpectation = &InviteServiceMockGetInfoExpectation{}
}
if mmGetInfo.defaultExpectation.params != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Expect")
}
if mmGetInfo.defaultExpectation.paramPtrs == nil {
mmGetInfo.defaultExpectation.paramPtrs = &InviteServiceMockGetInfoParamPtrs{}
}
mmGetInfo.defaultExpectation.paramPtrs.ctx = &ctx
mmGetInfo.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1)
return mmGetInfo
}
// ExpectCodeParam2 sets up expected param code for InviteService.GetInfo
func (mmGetInfo *mInviteServiceMockGetInfo) ExpectCodeParam2(code int64) *mInviteServiceMockGetInfo {
if mmGetInfo.mock.funcGetInfo != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Set")
}
if mmGetInfo.defaultExpectation == nil {
mmGetInfo.defaultExpectation = &InviteServiceMockGetInfoExpectation{}
}
if mmGetInfo.defaultExpectation.params != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Expect")
}
if mmGetInfo.defaultExpectation.paramPtrs == nil {
mmGetInfo.defaultExpectation.paramPtrs = &InviteServiceMockGetInfoParamPtrs{}
}
mmGetInfo.defaultExpectation.paramPtrs.code = &code
mmGetInfo.defaultExpectation.expectationOrigins.originCode = minimock.CallerInfo(1)
return mmGetInfo
}
// Inspect accepts an inspector function that has same arguments as the InviteService.GetInfo
func (mmGetInfo *mInviteServiceMockGetInfo) Inspect(f func(ctx context.Context, code int64)) *mInviteServiceMockGetInfo {
if mmGetInfo.mock.inspectFuncGetInfo != nil {
mmGetInfo.mock.t.Fatalf("Inspect function is already set for InviteServiceMock.GetInfo")
}
mmGetInfo.mock.inspectFuncGetInfo = f
return mmGetInfo
}
// Return sets up results that will be returned by InviteService.GetInfo
func (mmGetInfo *mInviteServiceMockGetInfo) Return(ip1 *model.InviteCode, err error) *InviteServiceMock {
if mmGetInfo.mock.funcGetInfo != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Set")
}
if mmGetInfo.defaultExpectation == nil {
mmGetInfo.defaultExpectation = &InviteServiceMockGetInfoExpectation{mock: mmGetInfo.mock}
}
mmGetInfo.defaultExpectation.results = &InviteServiceMockGetInfoResults{ip1, err}
mmGetInfo.defaultExpectation.returnOrigin = minimock.CallerInfo(1)
return mmGetInfo.mock
}
// Set uses given function f to mock the InviteService.GetInfo method
func (mmGetInfo *mInviteServiceMockGetInfo) Set(f func(ctx context.Context, code int64) (ip1 *model.InviteCode, err error)) *InviteServiceMock {
if mmGetInfo.defaultExpectation != nil {
mmGetInfo.mock.t.Fatalf("Default expectation is already set for the InviteService.GetInfo method")
}
if len(mmGetInfo.expectations) > 0 {
mmGetInfo.mock.t.Fatalf("Some expectations are already set for the InviteService.GetInfo method")
}
mmGetInfo.mock.funcGetInfo = f
mmGetInfo.mock.funcGetInfoOrigin = minimock.CallerInfo(1)
return mmGetInfo.mock
}
// When sets expectation for the InviteService.GetInfo which will trigger the result defined by the following
// Then helper
func (mmGetInfo *mInviteServiceMockGetInfo) When(ctx context.Context, code int64) *InviteServiceMockGetInfoExpectation {
if mmGetInfo.mock.funcGetInfo != nil {
mmGetInfo.mock.t.Fatalf("InviteServiceMock.GetInfo mock is already set by Set")
}
expectation := &InviteServiceMockGetInfoExpectation{
mock: mmGetInfo.mock,
params: &InviteServiceMockGetInfoParams{ctx, code},
expectationOrigins: InviteServiceMockGetInfoExpectationOrigins{origin: minimock.CallerInfo(1)},
}
mmGetInfo.expectations = append(mmGetInfo.expectations, expectation)
return expectation
}
// Then sets up InviteService.GetInfo return parameters for the expectation previously defined by the When method
func (e *InviteServiceMockGetInfoExpectation) Then(ip1 *model.InviteCode, err error) *InviteServiceMock {
e.results = &InviteServiceMockGetInfoResults{ip1, err}
return e.mock
}
// Times sets number of times InviteService.GetInfo should be invoked
func (mmGetInfo *mInviteServiceMockGetInfo) Times(n uint64) *mInviteServiceMockGetInfo {
if n == 0 {
mmGetInfo.mock.t.Fatalf("Times of InviteServiceMock.GetInfo mock can not be zero")
}
mm_atomic.StoreUint64(&mmGetInfo.expectedInvocations, n)
mmGetInfo.expectedInvocationsOrigin = minimock.CallerInfo(1)
return mmGetInfo
}
func (mmGetInfo *mInviteServiceMockGetInfo) invocationsDone() bool {
if len(mmGetInfo.expectations) == 0 && mmGetInfo.defaultExpectation == nil && mmGetInfo.mock.funcGetInfo == nil {
return true
}
totalInvocations := mm_atomic.LoadUint64(&mmGetInfo.mock.afterGetInfoCounter)
expectedInvocations := mm_atomic.LoadUint64(&mmGetInfo.expectedInvocations)
return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations)
}
// GetInfo implements mm_service.InviteService
func (mmGetInfo *InviteServiceMock) GetInfo(ctx context.Context, code int64) (ip1 *model.InviteCode, err error) {
mm_atomic.AddUint64(&mmGetInfo.beforeGetInfoCounter, 1)
defer mm_atomic.AddUint64(&mmGetInfo.afterGetInfoCounter, 1)
mmGetInfo.t.Helper()
if mmGetInfo.inspectFuncGetInfo != nil {
mmGetInfo.inspectFuncGetInfo(ctx, code)
}
mm_params := InviteServiceMockGetInfoParams{ctx, code}
// Record call args
mmGetInfo.GetInfoMock.mutex.Lock()
mmGetInfo.GetInfoMock.callArgs = append(mmGetInfo.GetInfoMock.callArgs, &mm_params)
mmGetInfo.GetInfoMock.mutex.Unlock()
for _, e := range mmGetInfo.GetInfoMock.expectations {
if minimock.Equal(*e.params, mm_params) {
mm_atomic.AddUint64(&e.Counter, 1)
return e.results.ip1, e.results.err
}
}
if mmGetInfo.GetInfoMock.defaultExpectation != nil {
mm_atomic.AddUint64(&mmGetInfo.GetInfoMock.defaultExpectation.Counter, 1)
mm_want := mmGetInfo.GetInfoMock.defaultExpectation.params
mm_want_ptrs := mmGetInfo.GetInfoMock.defaultExpectation.paramPtrs
mm_got := InviteServiceMockGetInfoParams{ctx, code}
if mm_want_ptrs != nil {
if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) {
mmGetInfo.t.Errorf("InviteServiceMock.GetInfo got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGetInfo.GetInfoMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx))
}
if mm_want_ptrs.code != nil && !minimock.Equal(*mm_want_ptrs.code, mm_got.code) {
mmGetInfo.t.Errorf("InviteServiceMock.GetInfo got unexpected parameter code, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGetInfo.GetInfoMock.defaultExpectation.expectationOrigins.originCode, *mm_want_ptrs.code, mm_got.code, minimock.Diff(*mm_want_ptrs.code, mm_got.code))
}
} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
mmGetInfo.t.Errorf("InviteServiceMock.GetInfo got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmGetInfo.GetInfoMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
}
mm_results := mmGetInfo.GetInfoMock.defaultExpectation.results
if mm_results == nil {
mmGetInfo.t.Fatal("No results are set for the InviteServiceMock.GetInfo")
}
return (*mm_results).ip1, (*mm_results).err
}
if mmGetInfo.funcGetInfo != nil {
return mmGetInfo.funcGetInfo(ctx, code)
}
mmGetInfo.t.Fatalf("Unexpected call to InviteServiceMock.GetInfo. %v %v", ctx, code)
return
}
// GetInfoAfterCounter returns a count of finished InviteServiceMock.GetInfo invocations
func (mmGetInfo *InviteServiceMock) GetInfoAfterCounter() uint64 {
return mm_atomic.LoadUint64(&mmGetInfo.afterGetInfoCounter)
}
// GetInfoBeforeCounter returns a count of InviteServiceMock.GetInfo invocations
func (mmGetInfo *InviteServiceMock) GetInfoBeforeCounter() uint64 {
return mm_atomic.LoadUint64(&mmGetInfo.beforeGetInfoCounter)
}
// Calls returns a list of arguments used in each call to InviteServiceMock.GetInfo.
// The list is in the same order as the calls were made (i.e. recent calls have a higher index)
func (mmGetInfo *mInviteServiceMockGetInfo) Calls() []*InviteServiceMockGetInfoParams {
mmGetInfo.mutex.RLock()
argCopy := make([]*InviteServiceMockGetInfoParams, len(mmGetInfo.callArgs))
copy(argCopy, mmGetInfo.callArgs)
mmGetInfo.mutex.RUnlock()
return argCopy
}
// MinimockGetInfoDone returns true if the count of the GetInfo invocations corresponds
// the number of defined expectations
func (m *InviteServiceMock) MinimockGetInfoDone() bool {
if m.GetInfoMock.optional {
// Optional methods provide '0 or more' call count restriction.
return true
}
for _, e := range m.GetInfoMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
return false
}
}
return m.GetInfoMock.invocationsDone()
}
// MinimockGetInfoInspect logs each unmet expectation
func (m *InviteServiceMock) MinimockGetInfoInspect() {
for _, e := range m.GetInfoMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
m.t.Errorf("Expected call to InviteServiceMock.GetInfo at\n%s with params: %#v", e.expectationOrigins.origin, *e.params)
}
}
afterGetInfoCounter := mm_atomic.LoadUint64(&m.afterGetInfoCounter)
// if default expectation was set then invocations count should be greater than zero
if m.GetInfoMock.defaultExpectation != nil && afterGetInfoCounter < 1 {
if m.GetInfoMock.defaultExpectation.params == nil {
m.t.Errorf("Expected call to InviteServiceMock.GetInfo at\n%s", m.GetInfoMock.defaultExpectation.returnOrigin)
} else {
m.t.Errorf("Expected call to InviteServiceMock.GetInfo at\n%s with params: %#v", m.GetInfoMock.defaultExpectation.expectationOrigins.origin, *m.GetInfoMock.defaultExpectation.params)
}
}
// if func was set then invocations count should be greater than zero
if m.funcGetInfo != nil && afterGetInfoCounter < 1 {
m.t.Errorf("Expected call to InviteServiceMock.GetInfo at\n%s", m.funcGetInfoOrigin)
}
if !m.GetInfoMock.invocationsDone() && afterGetInfoCounter > 0 {
m.t.Errorf("Expected %d calls to InviteServiceMock.GetInfo at\n%s but found %d calls",
mm_atomic.LoadUint64(&m.GetInfoMock.expectedInvocations), m.GetInfoMock.expectedInvocationsOrigin, afterGetInfoCounter)
}
}
// MinimockFinish checks that all mocked methods have been called the expected number of times
func (m *InviteServiceMock) MinimockFinish() {
m.finishOnce.Do(func() {
if !m.minimockDone() {
m.MinimockGenerateInspect()
m.MinimockGetInfoInspect()
}
})
}
// MinimockWait waits for all mocked methods to be called the expected number of times
func (m *InviteServiceMock) MinimockWait(timeout mm_time.Duration) {
timeoutCh := mm_time.After(timeout)
for {
if m.minimockDone() {
return
}
select {
case <-timeoutCh:
m.MinimockFinish()
return
case <-mm_time.After(10 * mm_time.Millisecond):
}
}
}
func (m *InviteServiceMock) minimockDone() bool {
done := true
return done &&
m.MinimockGenerateDone() &&
m.MinimockGetInfoDone()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,418 @@
// Code generated by http://github.com/gojuno/minimock (v3.4.7). DO NOT EDIT.
package mocks
//go:generate minimock -i smart-search-back/internal/service.SupplierService -o supplier_service_mock.go -n SupplierServiceMock -p mocks
import (
"context"
"sync"
mm_atomic "sync/atomic"
mm_time "time"
"github.com/gojuno/minimock/v3"
"github.com/google/uuid"
)
// SupplierServiceMock implements mm_service.SupplierService
type SupplierServiceMock struct {
t minimock.Tester
finishOnce sync.Once
funcExportExcel func(ctx context.Context, requestID uuid.UUID) (ba1 []byte, err error)
funcExportExcelOrigin string
inspectFuncExportExcel func(ctx context.Context, requestID uuid.UUID)
afterExportExcelCounter uint64
beforeExportExcelCounter uint64
ExportExcelMock mSupplierServiceMockExportExcel
}
// NewSupplierServiceMock returns a mock for mm_service.SupplierService
func NewSupplierServiceMock(t minimock.Tester) *SupplierServiceMock {
m := &SupplierServiceMock{t: t}
if controller, ok := t.(minimock.MockController); ok {
controller.RegisterMocker(m)
}
m.ExportExcelMock = mSupplierServiceMockExportExcel{mock: m}
m.ExportExcelMock.callArgs = []*SupplierServiceMockExportExcelParams{}
t.Cleanup(m.MinimockFinish)
return m
}
type mSupplierServiceMockExportExcel struct {
optional bool
mock *SupplierServiceMock
defaultExpectation *SupplierServiceMockExportExcelExpectation
expectations []*SupplierServiceMockExportExcelExpectation
callArgs []*SupplierServiceMockExportExcelParams
mutex sync.RWMutex
expectedInvocations uint64
expectedInvocationsOrigin string
}
// SupplierServiceMockExportExcelExpectation specifies expectation struct of the SupplierService.ExportExcel
type SupplierServiceMockExportExcelExpectation struct {
mock *SupplierServiceMock
params *SupplierServiceMockExportExcelParams
paramPtrs *SupplierServiceMockExportExcelParamPtrs
expectationOrigins SupplierServiceMockExportExcelExpectationOrigins
results *SupplierServiceMockExportExcelResults
returnOrigin string
Counter uint64
}
// SupplierServiceMockExportExcelParams contains parameters of the SupplierService.ExportExcel
type SupplierServiceMockExportExcelParams struct {
ctx context.Context
requestID uuid.UUID
}
// SupplierServiceMockExportExcelParamPtrs contains pointers to parameters of the SupplierService.ExportExcel
type SupplierServiceMockExportExcelParamPtrs struct {
ctx *context.Context
requestID *uuid.UUID
}
// SupplierServiceMockExportExcelResults contains results of the SupplierService.ExportExcel
type SupplierServiceMockExportExcelResults struct {
ba1 []byte
err error
}
// SupplierServiceMockExportExcelOrigins contains origins of expectations of the SupplierService.ExportExcel
type SupplierServiceMockExportExcelExpectationOrigins struct {
origin string
originCtx string
originRequestID string
}
// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning
// the test will fail minimock's automatic final call check if the mocked method was not called at least once.
// Optional() makes method check to work in '0 or more' mode.
// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to
// catch the problems when the expected method call is totally skipped during test run.
func (mmExportExcel *mSupplierServiceMockExportExcel) Optional() *mSupplierServiceMockExportExcel {
mmExportExcel.optional = true
return mmExportExcel
}
// Expect sets up expected params for SupplierService.ExportExcel
func (mmExportExcel *mSupplierServiceMockExportExcel) Expect(ctx context.Context, requestID uuid.UUID) *mSupplierServiceMockExportExcel {
if mmExportExcel.mock.funcExportExcel != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Set")
}
if mmExportExcel.defaultExpectation == nil {
mmExportExcel.defaultExpectation = &SupplierServiceMockExportExcelExpectation{}
}
if mmExportExcel.defaultExpectation.paramPtrs != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by ExpectParams functions")
}
mmExportExcel.defaultExpectation.params = &SupplierServiceMockExportExcelParams{ctx, requestID}
mmExportExcel.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1)
for _, e := range mmExportExcel.expectations {
if minimock.Equal(e.params, mmExportExcel.defaultExpectation.params) {
mmExportExcel.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmExportExcel.defaultExpectation.params)
}
}
return mmExportExcel
}
// ExpectCtxParam1 sets up expected param ctx for SupplierService.ExportExcel
func (mmExportExcel *mSupplierServiceMockExportExcel) ExpectCtxParam1(ctx context.Context) *mSupplierServiceMockExportExcel {
if mmExportExcel.mock.funcExportExcel != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Set")
}
if mmExportExcel.defaultExpectation == nil {
mmExportExcel.defaultExpectation = &SupplierServiceMockExportExcelExpectation{}
}
if mmExportExcel.defaultExpectation.params != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Expect")
}
if mmExportExcel.defaultExpectation.paramPtrs == nil {
mmExportExcel.defaultExpectation.paramPtrs = &SupplierServiceMockExportExcelParamPtrs{}
}
mmExportExcel.defaultExpectation.paramPtrs.ctx = &ctx
mmExportExcel.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1)
return mmExportExcel
}
// ExpectRequestIDParam2 sets up expected param requestID for SupplierService.ExportExcel
func (mmExportExcel *mSupplierServiceMockExportExcel) ExpectRequestIDParam2(requestID uuid.UUID) *mSupplierServiceMockExportExcel {
if mmExportExcel.mock.funcExportExcel != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Set")
}
if mmExportExcel.defaultExpectation == nil {
mmExportExcel.defaultExpectation = &SupplierServiceMockExportExcelExpectation{}
}
if mmExportExcel.defaultExpectation.params != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Expect")
}
if mmExportExcel.defaultExpectation.paramPtrs == nil {
mmExportExcel.defaultExpectation.paramPtrs = &SupplierServiceMockExportExcelParamPtrs{}
}
mmExportExcel.defaultExpectation.paramPtrs.requestID = &requestID
mmExportExcel.defaultExpectation.expectationOrigins.originRequestID = minimock.CallerInfo(1)
return mmExportExcel
}
// Inspect accepts an inspector function that has same arguments as the SupplierService.ExportExcel
func (mmExportExcel *mSupplierServiceMockExportExcel) Inspect(f func(ctx context.Context, requestID uuid.UUID)) *mSupplierServiceMockExportExcel {
if mmExportExcel.mock.inspectFuncExportExcel != nil {
mmExportExcel.mock.t.Fatalf("Inspect function is already set for SupplierServiceMock.ExportExcel")
}
mmExportExcel.mock.inspectFuncExportExcel = f
return mmExportExcel
}
// Return sets up results that will be returned by SupplierService.ExportExcel
func (mmExportExcel *mSupplierServiceMockExportExcel) Return(ba1 []byte, err error) *SupplierServiceMock {
if mmExportExcel.mock.funcExportExcel != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Set")
}
if mmExportExcel.defaultExpectation == nil {
mmExportExcel.defaultExpectation = &SupplierServiceMockExportExcelExpectation{mock: mmExportExcel.mock}
}
mmExportExcel.defaultExpectation.results = &SupplierServiceMockExportExcelResults{ba1, err}
mmExportExcel.defaultExpectation.returnOrigin = minimock.CallerInfo(1)
return mmExportExcel.mock
}
// Set uses given function f to mock the SupplierService.ExportExcel method
func (mmExportExcel *mSupplierServiceMockExportExcel) Set(f func(ctx context.Context, requestID uuid.UUID) (ba1 []byte, err error)) *SupplierServiceMock {
if mmExportExcel.defaultExpectation != nil {
mmExportExcel.mock.t.Fatalf("Default expectation is already set for the SupplierService.ExportExcel method")
}
if len(mmExportExcel.expectations) > 0 {
mmExportExcel.mock.t.Fatalf("Some expectations are already set for the SupplierService.ExportExcel method")
}
mmExportExcel.mock.funcExportExcel = f
mmExportExcel.mock.funcExportExcelOrigin = minimock.CallerInfo(1)
return mmExportExcel.mock
}
// When sets expectation for the SupplierService.ExportExcel which will trigger the result defined by the following
// Then helper
func (mmExportExcel *mSupplierServiceMockExportExcel) When(ctx context.Context, requestID uuid.UUID) *SupplierServiceMockExportExcelExpectation {
if mmExportExcel.mock.funcExportExcel != nil {
mmExportExcel.mock.t.Fatalf("SupplierServiceMock.ExportExcel mock is already set by Set")
}
expectation := &SupplierServiceMockExportExcelExpectation{
mock: mmExportExcel.mock,
params: &SupplierServiceMockExportExcelParams{ctx, requestID},
expectationOrigins: SupplierServiceMockExportExcelExpectationOrigins{origin: minimock.CallerInfo(1)},
}
mmExportExcel.expectations = append(mmExportExcel.expectations, expectation)
return expectation
}
// Then sets up SupplierService.ExportExcel return parameters for the expectation previously defined by the When method
func (e *SupplierServiceMockExportExcelExpectation) Then(ba1 []byte, err error) *SupplierServiceMock {
e.results = &SupplierServiceMockExportExcelResults{ba1, err}
return e.mock
}
// Times sets number of times SupplierService.ExportExcel should be invoked
func (mmExportExcel *mSupplierServiceMockExportExcel) Times(n uint64) *mSupplierServiceMockExportExcel {
if n == 0 {
mmExportExcel.mock.t.Fatalf("Times of SupplierServiceMock.ExportExcel mock can not be zero")
}
mm_atomic.StoreUint64(&mmExportExcel.expectedInvocations, n)
mmExportExcel.expectedInvocationsOrigin = minimock.CallerInfo(1)
return mmExportExcel
}
func (mmExportExcel *mSupplierServiceMockExportExcel) invocationsDone() bool {
if len(mmExportExcel.expectations) == 0 && mmExportExcel.defaultExpectation == nil && mmExportExcel.mock.funcExportExcel == nil {
return true
}
totalInvocations := mm_atomic.LoadUint64(&mmExportExcel.mock.afterExportExcelCounter)
expectedInvocations := mm_atomic.LoadUint64(&mmExportExcel.expectedInvocations)
return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations)
}
// ExportExcel implements mm_service.SupplierService
func (mmExportExcel *SupplierServiceMock) ExportExcel(ctx context.Context, requestID uuid.UUID) (ba1 []byte, err error) {
mm_atomic.AddUint64(&mmExportExcel.beforeExportExcelCounter, 1)
defer mm_atomic.AddUint64(&mmExportExcel.afterExportExcelCounter, 1)
mmExportExcel.t.Helper()
if mmExportExcel.inspectFuncExportExcel != nil {
mmExportExcel.inspectFuncExportExcel(ctx, requestID)
}
mm_params := SupplierServiceMockExportExcelParams{ctx, requestID}
// Record call args
mmExportExcel.ExportExcelMock.mutex.Lock()
mmExportExcel.ExportExcelMock.callArgs = append(mmExportExcel.ExportExcelMock.callArgs, &mm_params)
mmExportExcel.ExportExcelMock.mutex.Unlock()
for _, e := range mmExportExcel.ExportExcelMock.expectations {
if minimock.Equal(*e.params, mm_params) {
mm_atomic.AddUint64(&e.Counter, 1)
return e.results.ba1, e.results.err
}
}
if mmExportExcel.ExportExcelMock.defaultExpectation != nil {
mm_atomic.AddUint64(&mmExportExcel.ExportExcelMock.defaultExpectation.Counter, 1)
mm_want := mmExportExcel.ExportExcelMock.defaultExpectation.params
mm_want_ptrs := mmExportExcel.ExportExcelMock.defaultExpectation.paramPtrs
mm_got := SupplierServiceMockExportExcelParams{ctx, requestID}
if mm_want_ptrs != nil {
if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) {
mmExportExcel.t.Errorf("SupplierServiceMock.ExportExcel got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmExportExcel.ExportExcelMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx))
}
if mm_want_ptrs.requestID != nil && !minimock.Equal(*mm_want_ptrs.requestID, mm_got.requestID) {
mmExportExcel.t.Errorf("SupplierServiceMock.ExportExcel got unexpected parameter requestID, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmExportExcel.ExportExcelMock.defaultExpectation.expectationOrigins.originRequestID, *mm_want_ptrs.requestID, mm_got.requestID, minimock.Diff(*mm_want_ptrs.requestID, mm_got.requestID))
}
} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
mmExportExcel.t.Errorf("SupplierServiceMock.ExportExcel got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmExportExcel.ExportExcelMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
}
mm_results := mmExportExcel.ExportExcelMock.defaultExpectation.results
if mm_results == nil {
mmExportExcel.t.Fatal("No results are set for the SupplierServiceMock.ExportExcel")
}
return (*mm_results).ba1, (*mm_results).err
}
if mmExportExcel.funcExportExcel != nil {
return mmExportExcel.funcExportExcel(ctx, requestID)
}
mmExportExcel.t.Fatalf("Unexpected call to SupplierServiceMock.ExportExcel. %v %v", ctx, requestID)
return
}
// ExportExcelAfterCounter returns a count of finished SupplierServiceMock.ExportExcel invocations
func (mmExportExcel *SupplierServiceMock) ExportExcelAfterCounter() uint64 {
return mm_atomic.LoadUint64(&mmExportExcel.afterExportExcelCounter)
}
// ExportExcelBeforeCounter returns a count of SupplierServiceMock.ExportExcel invocations
func (mmExportExcel *SupplierServiceMock) ExportExcelBeforeCounter() uint64 {
return mm_atomic.LoadUint64(&mmExportExcel.beforeExportExcelCounter)
}
// Calls returns a list of arguments used in each call to SupplierServiceMock.ExportExcel.
// The list is in the same order as the calls were made (i.e. recent calls have a higher index)
func (mmExportExcel *mSupplierServiceMockExportExcel) Calls() []*SupplierServiceMockExportExcelParams {
mmExportExcel.mutex.RLock()
argCopy := make([]*SupplierServiceMockExportExcelParams, len(mmExportExcel.callArgs))
copy(argCopy, mmExportExcel.callArgs)
mmExportExcel.mutex.RUnlock()
return argCopy
}
// MinimockExportExcelDone returns true if the count of the ExportExcel invocations corresponds
// the number of defined expectations
func (m *SupplierServiceMock) MinimockExportExcelDone() bool {
if m.ExportExcelMock.optional {
// Optional methods provide '0 or more' call count restriction.
return true
}
for _, e := range m.ExportExcelMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
return false
}
}
return m.ExportExcelMock.invocationsDone()
}
// MinimockExportExcelInspect logs each unmet expectation
func (m *SupplierServiceMock) MinimockExportExcelInspect() {
for _, e := range m.ExportExcelMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
m.t.Errorf("Expected call to SupplierServiceMock.ExportExcel at\n%s with params: %#v", e.expectationOrigins.origin, *e.params)
}
}
afterExportExcelCounter := mm_atomic.LoadUint64(&m.afterExportExcelCounter)
// if default expectation was set then invocations count should be greater than zero
if m.ExportExcelMock.defaultExpectation != nil && afterExportExcelCounter < 1 {
if m.ExportExcelMock.defaultExpectation.params == nil {
m.t.Errorf("Expected call to SupplierServiceMock.ExportExcel at\n%s", m.ExportExcelMock.defaultExpectation.returnOrigin)
} else {
m.t.Errorf("Expected call to SupplierServiceMock.ExportExcel at\n%s with params: %#v", m.ExportExcelMock.defaultExpectation.expectationOrigins.origin, *m.ExportExcelMock.defaultExpectation.params)
}
}
// if func was set then invocations count should be greater than zero
if m.funcExportExcel != nil && afterExportExcelCounter < 1 {
m.t.Errorf("Expected call to SupplierServiceMock.ExportExcel at\n%s", m.funcExportExcelOrigin)
}
if !m.ExportExcelMock.invocationsDone() && afterExportExcelCounter > 0 {
m.t.Errorf("Expected %d calls to SupplierServiceMock.ExportExcel at\n%s but found %d calls",
mm_atomic.LoadUint64(&m.ExportExcelMock.expectedInvocations), m.ExportExcelMock.expectedInvocationsOrigin, afterExportExcelCounter)
}
}
// MinimockFinish checks that all mocked methods have been called the expected number of times
func (m *SupplierServiceMock) MinimockFinish() {
m.finishOnce.Do(func() {
if !m.minimockDone() {
m.MinimockExportExcelInspect()
}
})
}
// MinimockWait waits for all mocked methods to be called the expected number of times
func (m *SupplierServiceMock) MinimockWait(timeout mm_time.Duration) {
timeoutCh := mm_time.After(timeout)
for {
if m.minimockDone() {
return
}
select {
case <-timeoutCh:
m.MinimockFinish()
return
case <-mm_time.After(10 * mm_time.Millisecond):
}
}
}
func (m *SupplierServiceMock) minimockDone() bool {
done := true
return done &&
m.MinimockExportExcelDone()
}

View File

@@ -0,0 +1,417 @@
// Code generated by http://github.com/gojuno/minimock (v3.4.7). DO NOT EDIT.
package mocks
//go:generate minimock -i smart-search-back/internal/repository.TokenUsageRepository -o token_usage_repository_mock.go -n TokenUsageRepositoryMock -p mocks
import (
"context"
"smart-search-back/internal/model"
"sync"
mm_atomic "sync/atomic"
mm_time "time"
"github.com/gojuno/minimock/v3"
)
// TokenUsageRepositoryMock implements mm_repository.TokenUsageRepository
type TokenUsageRepositoryMock struct {
t minimock.Tester
finishOnce sync.Once
funcCreate func(ctx context.Context, usage *model.TokenUsage) (err error)
funcCreateOrigin string
inspectFuncCreate func(ctx context.Context, usage *model.TokenUsage)
afterCreateCounter uint64
beforeCreateCounter uint64
CreateMock mTokenUsageRepositoryMockCreate
}
// NewTokenUsageRepositoryMock returns a mock for mm_repository.TokenUsageRepository
func NewTokenUsageRepositoryMock(t minimock.Tester) *TokenUsageRepositoryMock {
m := &TokenUsageRepositoryMock{t: t}
if controller, ok := t.(minimock.MockController); ok {
controller.RegisterMocker(m)
}
m.CreateMock = mTokenUsageRepositoryMockCreate{mock: m}
m.CreateMock.callArgs = []*TokenUsageRepositoryMockCreateParams{}
t.Cleanup(m.MinimockFinish)
return m
}
type mTokenUsageRepositoryMockCreate struct {
optional bool
mock *TokenUsageRepositoryMock
defaultExpectation *TokenUsageRepositoryMockCreateExpectation
expectations []*TokenUsageRepositoryMockCreateExpectation
callArgs []*TokenUsageRepositoryMockCreateParams
mutex sync.RWMutex
expectedInvocations uint64
expectedInvocationsOrigin string
}
// TokenUsageRepositoryMockCreateExpectation specifies expectation struct of the TokenUsageRepository.Create
type TokenUsageRepositoryMockCreateExpectation struct {
mock *TokenUsageRepositoryMock
params *TokenUsageRepositoryMockCreateParams
paramPtrs *TokenUsageRepositoryMockCreateParamPtrs
expectationOrigins TokenUsageRepositoryMockCreateExpectationOrigins
results *TokenUsageRepositoryMockCreateResults
returnOrigin string
Counter uint64
}
// TokenUsageRepositoryMockCreateParams contains parameters of the TokenUsageRepository.Create
type TokenUsageRepositoryMockCreateParams struct {
ctx context.Context
usage *model.TokenUsage
}
// TokenUsageRepositoryMockCreateParamPtrs contains pointers to parameters of the TokenUsageRepository.Create
type TokenUsageRepositoryMockCreateParamPtrs struct {
ctx *context.Context
usage **model.TokenUsage
}
// TokenUsageRepositoryMockCreateResults contains results of the TokenUsageRepository.Create
type TokenUsageRepositoryMockCreateResults struct {
err error
}
// TokenUsageRepositoryMockCreateOrigins contains origins of expectations of the TokenUsageRepository.Create
type TokenUsageRepositoryMockCreateExpectationOrigins struct {
origin string
originCtx string
originUsage string
}
// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning
// the test will fail minimock's automatic final call check if the mocked method was not called at least once.
// Optional() makes method check to work in '0 or more' mode.
// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to
// catch the problems when the expected method call is totally skipped during test run.
func (mmCreate *mTokenUsageRepositoryMockCreate) Optional() *mTokenUsageRepositoryMockCreate {
mmCreate.optional = true
return mmCreate
}
// Expect sets up expected params for TokenUsageRepository.Create
func (mmCreate *mTokenUsageRepositoryMockCreate) Expect(ctx context.Context, usage *model.TokenUsage) *mTokenUsageRepositoryMockCreate {
if mmCreate.mock.funcCreate != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Set")
}
if mmCreate.defaultExpectation == nil {
mmCreate.defaultExpectation = &TokenUsageRepositoryMockCreateExpectation{}
}
if mmCreate.defaultExpectation.paramPtrs != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by ExpectParams functions")
}
mmCreate.defaultExpectation.params = &TokenUsageRepositoryMockCreateParams{ctx, usage}
mmCreate.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1)
for _, e := range mmCreate.expectations {
if minimock.Equal(e.params, mmCreate.defaultExpectation.params) {
mmCreate.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmCreate.defaultExpectation.params)
}
}
return mmCreate
}
// ExpectCtxParam1 sets up expected param ctx for TokenUsageRepository.Create
func (mmCreate *mTokenUsageRepositoryMockCreate) ExpectCtxParam1(ctx context.Context) *mTokenUsageRepositoryMockCreate {
if mmCreate.mock.funcCreate != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Set")
}
if mmCreate.defaultExpectation == nil {
mmCreate.defaultExpectation = &TokenUsageRepositoryMockCreateExpectation{}
}
if mmCreate.defaultExpectation.params != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Expect")
}
if mmCreate.defaultExpectation.paramPtrs == nil {
mmCreate.defaultExpectation.paramPtrs = &TokenUsageRepositoryMockCreateParamPtrs{}
}
mmCreate.defaultExpectation.paramPtrs.ctx = &ctx
mmCreate.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1)
return mmCreate
}
// ExpectUsageParam2 sets up expected param usage for TokenUsageRepository.Create
func (mmCreate *mTokenUsageRepositoryMockCreate) ExpectUsageParam2(usage *model.TokenUsage) *mTokenUsageRepositoryMockCreate {
if mmCreate.mock.funcCreate != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Set")
}
if mmCreate.defaultExpectation == nil {
mmCreate.defaultExpectation = &TokenUsageRepositoryMockCreateExpectation{}
}
if mmCreate.defaultExpectation.params != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Expect")
}
if mmCreate.defaultExpectation.paramPtrs == nil {
mmCreate.defaultExpectation.paramPtrs = &TokenUsageRepositoryMockCreateParamPtrs{}
}
mmCreate.defaultExpectation.paramPtrs.usage = &usage
mmCreate.defaultExpectation.expectationOrigins.originUsage = minimock.CallerInfo(1)
return mmCreate
}
// Inspect accepts an inspector function that has same arguments as the TokenUsageRepository.Create
func (mmCreate *mTokenUsageRepositoryMockCreate) Inspect(f func(ctx context.Context, usage *model.TokenUsage)) *mTokenUsageRepositoryMockCreate {
if mmCreate.mock.inspectFuncCreate != nil {
mmCreate.mock.t.Fatalf("Inspect function is already set for TokenUsageRepositoryMock.Create")
}
mmCreate.mock.inspectFuncCreate = f
return mmCreate
}
// Return sets up results that will be returned by TokenUsageRepository.Create
func (mmCreate *mTokenUsageRepositoryMockCreate) Return(err error) *TokenUsageRepositoryMock {
if mmCreate.mock.funcCreate != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Set")
}
if mmCreate.defaultExpectation == nil {
mmCreate.defaultExpectation = &TokenUsageRepositoryMockCreateExpectation{mock: mmCreate.mock}
}
mmCreate.defaultExpectation.results = &TokenUsageRepositoryMockCreateResults{err}
mmCreate.defaultExpectation.returnOrigin = minimock.CallerInfo(1)
return mmCreate.mock
}
// Set uses given function f to mock the TokenUsageRepository.Create method
func (mmCreate *mTokenUsageRepositoryMockCreate) Set(f func(ctx context.Context, usage *model.TokenUsage) (err error)) *TokenUsageRepositoryMock {
if mmCreate.defaultExpectation != nil {
mmCreate.mock.t.Fatalf("Default expectation is already set for the TokenUsageRepository.Create method")
}
if len(mmCreate.expectations) > 0 {
mmCreate.mock.t.Fatalf("Some expectations are already set for the TokenUsageRepository.Create method")
}
mmCreate.mock.funcCreate = f
mmCreate.mock.funcCreateOrigin = minimock.CallerInfo(1)
return mmCreate.mock
}
// When sets expectation for the TokenUsageRepository.Create which will trigger the result defined by the following
// Then helper
func (mmCreate *mTokenUsageRepositoryMockCreate) When(ctx context.Context, usage *model.TokenUsage) *TokenUsageRepositoryMockCreateExpectation {
if mmCreate.mock.funcCreate != nil {
mmCreate.mock.t.Fatalf("TokenUsageRepositoryMock.Create mock is already set by Set")
}
expectation := &TokenUsageRepositoryMockCreateExpectation{
mock: mmCreate.mock,
params: &TokenUsageRepositoryMockCreateParams{ctx, usage},
expectationOrigins: TokenUsageRepositoryMockCreateExpectationOrigins{origin: minimock.CallerInfo(1)},
}
mmCreate.expectations = append(mmCreate.expectations, expectation)
return expectation
}
// Then sets up TokenUsageRepository.Create return parameters for the expectation previously defined by the When method
func (e *TokenUsageRepositoryMockCreateExpectation) Then(err error) *TokenUsageRepositoryMock {
e.results = &TokenUsageRepositoryMockCreateResults{err}
return e.mock
}
// Times sets number of times TokenUsageRepository.Create should be invoked
func (mmCreate *mTokenUsageRepositoryMockCreate) Times(n uint64) *mTokenUsageRepositoryMockCreate {
if n == 0 {
mmCreate.mock.t.Fatalf("Times of TokenUsageRepositoryMock.Create mock can not be zero")
}
mm_atomic.StoreUint64(&mmCreate.expectedInvocations, n)
mmCreate.expectedInvocationsOrigin = minimock.CallerInfo(1)
return mmCreate
}
func (mmCreate *mTokenUsageRepositoryMockCreate) invocationsDone() bool {
if len(mmCreate.expectations) == 0 && mmCreate.defaultExpectation == nil && mmCreate.mock.funcCreate == nil {
return true
}
totalInvocations := mm_atomic.LoadUint64(&mmCreate.mock.afterCreateCounter)
expectedInvocations := mm_atomic.LoadUint64(&mmCreate.expectedInvocations)
return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations)
}
// Create implements mm_repository.TokenUsageRepository
func (mmCreate *TokenUsageRepositoryMock) Create(ctx context.Context, usage *model.TokenUsage) (err error) {
mm_atomic.AddUint64(&mmCreate.beforeCreateCounter, 1)
defer mm_atomic.AddUint64(&mmCreate.afterCreateCounter, 1)
mmCreate.t.Helper()
if mmCreate.inspectFuncCreate != nil {
mmCreate.inspectFuncCreate(ctx, usage)
}
mm_params := TokenUsageRepositoryMockCreateParams{ctx, usage}
// Record call args
mmCreate.CreateMock.mutex.Lock()
mmCreate.CreateMock.callArgs = append(mmCreate.CreateMock.callArgs, &mm_params)
mmCreate.CreateMock.mutex.Unlock()
for _, e := range mmCreate.CreateMock.expectations {
if minimock.Equal(*e.params, mm_params) {
mm_atomic.AddUint64(&e.Counter, 1)
return e.results.err
}
}
if mmCreate.CreateMock.defaultExpectation != nil {
mm_atomic.AddUint64(&mmCreate.CreateMock.defaultExpectation.Counter, 1)
mm_want := mmCreate.CreateMock.defaultExpectation.params
mm_want_ptrs := mmCreate.CreateMock.defaultExpectation.paramPtrs
mm_got := TokenUsageRepositoryMockCreateParams{ctx, usage}
if mm_want_ptrs != nil {
if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) {
mmCreate.t.Errorf("TokenUsageRepositoryMock.Create got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmCreate.CreateMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx))
}
if mm_want_ptrs.usage != nil && !minimock.Equal(*mm_want_ptrs.usage, mm_got.usage) {
mmCreate.t.Errorf("TokenUsageRepositoryMock.Create got unexpected parameter usage, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmCreate.CreateMock.defaultExpectation.expectationOrigins.originUsage, *mm_want_ptrs.usage, mm_got.usage, minimock.Diff(*mm_want_ptrs.usage, mm_got.usage))
}
} else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
mmCreate.t.Errorf("TokenUsageRepositoryMock.Create got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n",
mmCreate.CreateMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
}
mm_results := mmCreate.CreateMock.defaultExpectation.results
if mm_results == nil {
mmCreate.t.Fatal("No results are set for the TokenUsageRepositoryMock.Create")
}
return (*mm_results).err
}
if mmCreate.funcCreate != nil {
return mmCreate.funcCreate(ctx, usage)
}
mmCreate.t.Fatalf("Unexpected call to TokenUsageRepositoryMock.Create. %v %v", ctx, usage)
return
}
// CreateAfterCounter returns a count of finished TokenUsageRepositoryMock.Create invocations
func (mmCreate *TokenUsageRepositoryMock) CreateAfterCounter() uint64 {
return mm_atomic.LoadUint64(&mmCreate.afterCreateCounter)
}
// CreateBeforeCounter returns a count of TokenUsageRepositoryMock.Create invocations
func (mmCreate *TokenUsageRepositoryMock) CreateBeforeCounter() uint64 {
return mm_atomic.LoadUint64(&mmCreate.beforeCreateCounter)
}
// Calls returns a list of arguments used in each call to TokenUsageRepositoryMock.Create.
// The list is in the same order as the calls were made (i.e. recent calls have a higher index)
func (mmCreate *mTokenUsageRepositoryMockCreate) Calls() []*TokenUsageRepositoryMockCreateParams {
mmCreate.mutex.RLock()
argCopy := make([]*TokenUsageRepositoryMockCreateParams, len(mmCreate.callArgs))
copy(argCopy, mmCreate.callArgs)
mmCreate.mutex.RUnlock()
return argCopy
}
// MinimockCreateDone returns true if the count of the Create invocations corresponds
// the number of defined expectations
func (m *TokenUsageRepositoryMock) MinimockCreateDone() bool {
if m.CreateMock.optional {
// Optional methods provide '0 or more' call count restriction.
return true
}
for _, e := range m.CreateMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
return false
}
}
return m.CreateMock.invocationsDone()
}
// MinimockCreateInspect logs each unmet expectation
func (m *TokenUsageRepositoryMock) MinimockCreateInspect() {
for _, e := range m.CreateMock.expectations {
if mm_atomic.LoadUint64(&e.Counter) < 1 {
m.t.Errorf("Expected call to TokenUsageRepositoryMock.Create at\n%s with params: %#v", e.expectationOrigins.origin, *e.params)
}
}
afterCreateCounter := mm_atomic.LoadUint64(&m.afterCreateCounter)
// if default expectation was set then invocations count should be greater than zero
if m.CreateMock.defaultExpectation != nil && afterCreateCounter < 1 {
if m.CreateMock.defaultExpectation.params == nil {
m.t.Errorf("Expected call to TokenUsageRepositoryMock.Create at\n%s", m.CreateMock.defaultExpectation.returnOrigin)
} else {
m.t.Errorf("Expected call to TokenUsageRepositoryMock.Create at\n%s with params: %#v", m.CreateMock.defaultExpectation.expectationOrigins.origin, *m.CreateMock.defaultExpectation.params)
}
}
// if func was set then invocations count should be greater than zero
if m.funcCreate != nil && afterCreateCounter < 1 {
m.t.Errorf("Expected call to TokenUsageRepositoryMock.Create at\n%s", m.funcCreateOrigin)
}
if !m.CreateMock.invocationsDone() && afterCreateCounter > 0 {
m.t.Errorf("Expected %d calls to TokenUsageRepositoryMock.Create at\n%s but found %d calls",
mm_atomic.LoadUint64(&m.CreateMock.expectedInvocations), m.CreateMock.expectedInvocationsOrigin, afterCreateCounter)
}
}
// MinimockFinish checks that all mocked methods have been called the expected number of times
func (m *TokenUsageRepositoryMock) MinimockFinish() {
m.finishOnce.Do(func() {
if !m.minimockDone() {
m.MinimockCreateInspect()
}
})
}
// MinimockWait waits for all mocked methods to be called the expected number of times
func (m *TokenUsageRepositoryMock) MinimockWait(timeout mm_time.Duration) {
timeoutCh := mm_time.After(timeout)
for {
if m.minimockDone() {
return
}
select {
case <-timeoutCh:
m.MinimockFinish()
return
case <-mm_time.After(10 * mm_time.Millisecond):
}
}
}
func (m *TokenUsageRepositoryMock) minimockDone() bool {
done := true
return done &&
m.MinimockCreateDone()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

14
internal/model/invite.go Normal file
View File

@@ -0,0 +1,14 @@
package model
import "time"
type InviteCode struct {
ID int
UserID int
Code int64
CanBeUsedCount int
UsedCount int
IsActive bool
CreatedAt time.Time
ExpiresAt time.Time
}

40
internal/model/request.go Normal file
View File

@@ -0,0 +1,40 @@
package model
import (
"time"
"github.com/google/uuid"
)
type Request struct {
ID uuid.UUID
UserID int
RequestTxt string
GeneratedTZ bool
FinalTZ string
GeneratedFinalTZ bool
FinalUpdateTZ string
MailingStatusID int
MailingStatus string
CreatedAt time.Time
}
type MailingStatus struct {
ID int
StatusName string
}
type RequestDetail struct {
RequestID uuid.UUID
Title string
MailText string
Suppliers []SupplierInfo
}
type SupplierInfo struct {
CompanyID int `json:"company_id"`
Email string `json:"email"`
Phone string `json:"phone"`
CompanyName string `json:"company_name"`
URL string `json:"url"`
}

15
internal/model/session.go Normal file
View File

@@ -0,0 +1,15 @@
package model
import "time"
type Session struct {
ID int
UserID int
AccessToken string
RefreshToken string
IP string
UserAgent string
CreatedAt time.Time
ExpiresAt time.Time
RevokedAt *time.Time
}

View File

@@ -0,0 +1,28 @@
package model
import (
"time"
"github.com/google/uuid"
)
type Supplier struct {
ID int
RequestID uuid.UUID
Name string
Email string
Phone string
Address string
URL string
CreatedAt time.Time
}
type TokenUsage struct {
ID int
RequestID uuid.UUID
RequestTokenCount int
ResponseTokenCount int
TokenCost float64
Type string
CreatedAt time.Time
}

18
internal/model/user.go Normal file
View File

@@ -0,0 +1,18 @@
package model
import "time"
type User struct {
ID int
Email string
EmailHash string
PasswordHash string
Phone string
UserName string
CompanyName string
Balance float64
PaymentStatus string
InvitesIssued int
InvitesLimit int
CreatedAt time.Time
}

View File

@@ -0,0 +1,54 @@
package repository
import (
"context"
"github.com/google/uuid"
"smart-search-back/internal/model"
)
type UserRepository interface {
FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error)
FindByID(ctx context.Context, userID int) (*model.User, error)
Create(ctx context.Context, user *model.User) error
UpdateBalance(ctx context.Context, userID int, delta float64) error
GetBalance(ctx context.Context, userID int) (float64, error)
IncrementInvitesIssued(ctx context.Context, userID int) error
CheckInviteLimit(ctx context.Context, userID int) (bool, error)
}
type SessionRepository interface {
Create(ctx context.Context, session *model.Session) error
FindByRefreshToken(ctx context.Context, token string) (*model.Session, error)
UpdateAccessToken(ctx context.Context, refreshToken, newAccessToken string) error
Revoke(ctx context.Context, refreshToken string) error
DeleteExpired(ctx context.Context) (int, error)
}
type InviteRepository interface {
Create(ctx context.Context, invite *model.InviteCode) error
FindByCode(ctx context.Context, code int64) (*model.InviteCode, error)
IncrementUsedCount(ctx context.Context, code int64) error
DeactivateExpired(ctx context.Context) (int, error)
GetUserInvites(ctx context.Context, userID int) ([]*model.InviteCode, error)
}
type RequestRepository interface {
Create(ctx context.Context, req *model.Request) error
UpdateWithTZ(ctx context.Context, id uuid.UUID, tz string, generated bool) error
UpdateFinalTZ(ctx context.Context, id uuid.UUID, finalTZ string) error
GetByUserID(ctx context.Context, userID int) ([]*model.Request, error)
GetByID(ctx context.Context, id uuid.UUID) (*model.Request, error)
GetDetailByID(ctx context.Context, id uuid.UUID) (*model.RequestDetail, error)
GetUserStatistics(ctx context.Context, userID int) (requestsCount, suppliersCount, createdTZ int, err error)
}
type SupplierRepository interface {
BulkInsert(ctx context.Context, requestID uuid.UUID, suppliers []*model.Supplier) error
GetByRequestID(ctx context.Context, requestID uuid.UUID) ([]*model.Supplier, error)
DeleteByRequestID(ctx context.Context, requestID uuid.UUID) error
}
type TokenUsageRepository interface {
Create(ctx context.Context, usage *model.TokenUsage) error
}

View File

@@ -0,0 +1,146 @@
package repository
import (
"context"
"errors"
"smart-search-back/internal/model"
errs "smart-search-back/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type inviteRepository struct {
pool *pgxpool.Pool
qb sq.StatementBuilderType
}
func NewInviteRepository(pool *pgxpool.Pool) InviteRepository {
return &inviteRepository{
pool: pool,
qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
}
func (r *inviteRepository) Create(ctx context.Context, invite *model.InviteCode) error {
query := r.qb.Insert("invite_codes").Columns(
"user_id", "code", "can_be_used_count", "expires_at",
).Values(
invite.UserID, invite.Code, invite.CanBeUsedCount, invite.ExpiresAt,
).Suffix("RETURNING id, created_at")
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&invite.ID, &invite.CreatedAt)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to create invite code", err)
}
return nil
}
func (r *inviteRepository) FindByCode(ctx context.Context, code int64) (*model.InviteCode, error) {
query := r.qb.Select(
"id", "user_id", "code", "can_be_used_count", "used_count",
"is_active", "created_at", "expires_at",
).From("invite_codes").Where(sq.Eq{"code": code})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
invite := &model.InviteCode{}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(
&invite.ID, &invite.UserID, &invite.Code, &invite.CanBeUsedCount,
&invite.UsedCount, &invite.IsActive, &invite.CreatedAt, &invite.ExpiresAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.NewBusinessError(errs.UserNotFound, "invite code not found")
}
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to find invite code", err)
}
return invite, nil
}
func (r *inviteRepository) IncrementUsedCount(ctx context.Context, code int64) error {
query := r.qb.Update("invite_codes").
Set("used_count", sq.Expr("used_count + 1")).
Where(sq.Eq{"code": code})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to increment used count", err)
}
return nil
}
func (r *inviteRepository) DeactivateExpired(ctx context.Context) (int, error) {
query := r.qb.Update("invite_codes").
Set("is_active", false).
Where(sq.And{
sq.Expr("expires_at < now()"),
sq.Eq{"is_active": true},
})
sqlQuery, args, err := query.ToSql()
if err != nil {
return 0, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
result, err := r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return 0, errs.NewInternalError(errs.DatabaseError, "failed to deactivate expired invites", err)
}
return int(result.RowsAffected()), nil
}
func (r *inviteRepository) GetUserInvites(ctx context.Context, userID int) ([]*model.InviteCode, error) {
query := r.qb.Select(
"id", "user_id", "code", "can_be_used_count", "used_count",
"is_active", "created_at", "expires_at",
).From("invite_codes").
Where(sq.Eq{"user_id": userID}).
OrderBy("created_at DESC")
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
rows, err := r.pool.Query(ctx, sqlQuery, args...)
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to get user invites", err)
}
defer rows.Close()
var invites []*model.InviteCode
for rows.Next() {
invite := &model.InviteCode{}
err := rows.Scan(
&invite.ID, &invite.UserID, &invite.Code, &invite.CanBeUsedCount,
&invite.UsedCount, &invite.IsActive, &invite.CreatedAt, &invite.ExpiresAt,
)
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to scan invite", err)
}
invites = append(invites, invite)
}
return invites, nil
}

View File

@@ -0,0 +1,209 @@
package repository
import (
"context"
"encoding/json"
"errors"
"smart-search-back/internal/model"
errs "smart-search-back/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type requestRepository struct {
pool *pgxpool.Pool
qb sq.StatementBuilderType
}
func NewRequestRepository(pool *pgxpool.Pool) RequestRepository {
return &requestRepository{
pool: pool,
qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
}
func (r *requestRepository) Create(ctx context.Context, req *model.Request) error {
query := r.qb.Insert("requests_for_suppliers").Columns(
"user_id", "request_txt",
).Values(
req.UserID, req.RequestTxt,
).Suffix("RETURNING id, created_at")
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&req.ID, &req.CreatedAt)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to create request", err)
}
return nil
}
func (r *requestRepository) UpdateWithTZ(ctx context.Context, id uuid.UUID, tz string, generated bool) error {
query := r.qb.Update("requests_for_suppliers").
Set("final_tz", tz).
Set("generated_tz", generated).
Set("generated_final_tz", generated).
Where(sq.Eq{"id": id})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to update request", err)
}
return nil
}
func (r *requestRepository) UpdateFinalTZ(ctx context.Context, id uuid.UUID, finalTZ string) error {
query := r.qb.Update("requests_for_suppliers").
Set("final_update_tz", finalTZ).
Where(sq.Eq{"id": id})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to update final TZ", err)
}
return nil
}
func (r *requestRepository) GetByUserID(ctx context.Context, userID int) ([]*model.Request, error) {
query := r.qb.Select(
"r.id", "r.request_txt", "ms.status_name as mailing_status",
).From("requests_for_suppliers r").
Join("mailing_status ms ON r.mailling_status_id = ms.id").
Where(sq.Eq{"r.user_id": userID}).
OrderBy("r.created_at DESC")
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
rows, err := r.pool.Query(ctx, sqlQuery, args...)
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to get requests", err)
}
defer rows.Close()
var requests []*model.Request
for rows.Next() {
req := &model.Request{}
var statusName string
err := rows.Scan(&req.ID, &req.RequestTxt, &statusName)
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to scan request", err)
}
requests = append(requests, req)
}
return requests, nil
}
func (r *requestRepository) GetByID(ctx context.Context, id uuid.UUID) (*model.Request, error) {
query := r.qb.Select(
"id", "user_id", "request_txt", "generated_tz", "final_tz",
"generated_final_tz", "final_update_tz", "mailling_status_id", "created_at",
).From("requests_for_suppliers").Where(sq.Eq{"id": id})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
req := &model.Request{}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(
&req.ID, &req.UserID, &req.RequestTxt, &req.GeneratedTZ,
&req.FinalTZ, &req.GeneratedFinalTZ, &req.FinalUpdateTZ,
&req.MailingStatusID, &req.CreatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.NewBusinessError(errs.RequestNotFound, "request not found")
}
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to get request", err)
}
return req, nil
}
func (r *requestRepository) GetDetailByID(ctx context.Context, id uuid.UUID) (*model.RequestDetail, error) {
sqlQuery := `
SELECT
r.id AS request_id,
r.request_txt AS title,
r.final_update_tz AS mail_text,
COALESCE(json_agg(
json_build_object(
'email', COALESCE(s.email, ''),
'phone', COALESCE(s.phone, ''),
'company_name', COALESCE(s.name, ''),
'company_id', s.id,
'url', COALESCE(s.url, '')
)
) FILTER (WHERE s.id IS NOT NULL), '[]') AS suppliers
FROM requests_for_suppliers r
LEFT JOIN suppliers s ON s.request_id = r.id
WHERE r.id = $1
GROUP BY r.id, r.request_txt, r.final_update_tz
`
detail := &model.RequestDetail{}
var suppliersJSON []byte
err := r.pool.QueryRow(ctx, sqlQuery, id).Scan(
&detail.RequestID, &detail.Title, &detail.MailText, &suppliersJSON,
)
if err == pgx.ErrNoRows {
return nil, errs.NewBusinessError(errs.RequestNotFound, "request not found")
}
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to get request detail", err)
}
if len(suppliersJSON) > 0 && string(suppliersJSON) != "[]" {
if err := json.Unmarshal(suppliersJSON, &detail.Suppliers); err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to parse suppliers", err)
}
}
return detail, nil
}
func (r *requestRepository) GetUserStatistics(ctx context.Context, userID int) (requestsCount, suppliersCount, createdTZ int, err error) {
sqlQuery := `
SELECT
COUNT(DISTINCT r.id) AS requests_count,
COUNT(s.id) AS suppliers_count,
COUNT(r.request_txt) AS created_tz
FROM requests_for_suppliers r
LEFT JOIN suppliers s ON s.request_id = r.id
WHERE r.user_id = $1
`
err = r.pool.QueryRow(ctx, sqlQuery, userID).Scan(&requestsCount, &suppliersCount, &createdTZ)
if err != nil {
return 0, 0, 0, errs.NewInternalError(errs.DatabaseError, "failed to get statistics", err)
}
return requestsCount, suppliersCount, createdTZ, nil
}

View File

@@ -0,0 +1,134 @@
package repository
import (
"context"
"errors"
"time"
"smart-search-back/internal/model"
errs "smart-search-back/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type sessionRepository struct {
pool *pgxpool.Pool
qb sq.StatementBuilderType
}
func NewSessionRepository(pool *pgxpool.Pool) SessionRepository {
return &sessionRepository{
pool: pool,
qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
}
func (r *sessionRepository) Create(ctx context.Context, session *model.Session) error {
query := r.qb.Insert("sessions").Columns(
"user_id", "access_token", "refresh_token", "ip", "user_agent", "expires_at",
).Values(
session.UserID, session.AccessToken, session.RefreshToken,
session.IP, session.UserAgent, session.ExpiresAt,
).Suffix("RETURNING id")
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&session.ID)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to create session", err)
}
return nil
}
func (r *sessionRepository) FindByRefreshToken(ctx context.Context, token string) (*model.Session, error) {
query := r.qb.Select(
"id", "user_id", "access_token", "refresh_token", "ip",
"user_agent", "created_at", "expires_at", "revoked_at",
).From("sessions").Where(sq.And{
sq.Eq{"refresh_token": token},
sq.Expr("revoked_at IS NULL"),
sq.Expr("expires_at > now()"),
})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
session := &model.Session{}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(
&session.ID, &session.UserID, &session.AccessToken, &session.RefreshToken,
&session.IP, &session.UserAgent, &session.CreatedAt, &session.ExpiresAt,
&session.RevokedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.NewBusinessError(errs.RefreshInvalid, "refresh token is invalid or expired")
}
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to find session", err)
}
return session, nil
}
func (r *sessionRepository) UpdateAccessToken(ctx context.Context, refreshToken, newAccessToken string) error {
query := r.qb.Update("sessions").
Set("access_token", newAccessToken).
Where(sq.Eq{"refresh_token": refreshToken})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to update access token", err)
}
return nil
}
func (r *sessionRepository) Revoke(ctx context.Context, refreshToken string) error {
query := r.qb.Update("sessions").
Set("revoked_at", time.Now()).
Where(sq.Eq{"refresh_token": refreshToken})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to revoke session", err)
}
return nil
}
func (r *sessionRepository) DeleteExpired(ctx context.Context) (int, error) {
query := r.qb.Delete("sessions").Where(sq.Or{
sq.Expr("expires_at < now()"),
sq.Expr("(revoked_at IS NOT NULL AND revoked_at < now() - interval '30 days')"),
})
sqlQuery, args, err := query.ToSql()
if err != nil {
return 0, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
result, err := r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return 0, errs.NewInternalError(errs.DatabaseError, "failed to delete expired sessions", err)
}
return int(result.RowsAffected()), nil
}

View File

@@ -0,0 +1,97 @@
package repository
import (
"context"
"smart-search-back/internal/model"
errs "smart-search-back/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type supplierRepository struct {
pool *pgxpool.Pool
qb sq.StatementBuilderType
}
func NewSupplierRepository(pool *pgxpool.Pool) SupplierRepository {
return &supplierRepository{
pool: pool,
qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
}
func (r *supplierRepository) BulkInsert(ctx context.Context, requestID uuid.UUID, suppliers []*model.Supplier) error {
if len(suppliers) == 0 {
return nil
}
query := r.qb.Insert("suppliers").Columns(
"request_id", "name", "email", "phone", "adress", "url",
)
for _, s := range suppliers {
query = query.Values(requestID, s.Name, s.Email, s.Phone, s.Address, s.URL)
}
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to bulk insert suppliers", err)
}
return nil
}
func (r *supplierRepository) GetByRequestID(ctx context.Context, requestID uuid.UUID) ([]*model.Supplier, error) {
query := r.qb.Select(
"id", "request_id", "name", "email", "phone", "adress", "url", "created_at",
).From("suppliers").Where(sq.Eq{"request_id": requestID}).OrderBy("id")
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
rows, err := r.pool.Query(ctx, sqlQuery, args...)
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to get suppliers", err)
}
defer rows.Close()
var suppliers []*model.Supplier
for rows.Next() {
s := &model.Supplier{}
err := rows.Scan(
&s.ID, &s.RequestID, &s.Name, &s.Email, &s.Phone, &s.Address, &s.URL, &s.CreatedAt,
)
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to scan supplier", err)
}
suppliers = append(suppliers, s)
}
return suppliers, nil
}
func (r *supplierRepository) DeleteByRequestID(ctx context.Context, requestID uuid.UUID) error {
query := r.qb.Delete("suppliers").Where(sq.Eq{"request_id": requestID})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to delete suppliers", err)
}
return nil
}

View File

@@ -0,0 +1,43 @@
package repository
import (
"context"
"smart-search-back/internal/model"
errs "smart-search-back/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5/pgxpool"
)
type tokenUsageRepository struct {
pool *pgxpool.Pool
qb sq.StatementBuilderType
}
func NewTokenUsageRepository(pool *pgxpool.Pool) TokenUsageRepository {
return &tokenUsageRepository{
pool: pool,
qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
}
func (r *tokenUsageRepository) Create(ctx context.Context, usage *model.TokenUsage) error {
query := r.qb.Insert("request_token_usage").Columns(
"request_id", "request_token_count", "response_token_count", "token_cost", "type",
).Values(
usage.RequestID, usage.RequestTokenCount, usage.ResponseTokenCount, usage.TokenCost, usage.Type,
).Suffix("RETURNING id, created_at")
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&usage.ID, &usage.CreatedAt)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to create token usage", err)
}
return nil
}

199
internal/repository/user.go Normal file
View File

@@ -0,0 +1,199 @@
package repository
import (
"context"
"errors"
"smart-search-back/internal/model"
"smart-search-back/pkg/crypto"
errs "smart-search-back/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type userRepository struct {
pool *pgxpool.Pool
qb sq.StatementBuilderType
cryptoHelper *crypto.Crypto
}
func NewUserRepository(pool *pgxpool.Pool, cryptoSecret string) UserRepository {
return &userRepository{
pool: pool,
qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
cryptoHelper: crypto.NewCrypto(cryptoSecret),
}
}
func (r *userRepository) FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error) {
query := r.qb.Select(
"id", "email", "email_hash", "password_hash", "phone",
"user_name", "company_name", "balance", "payment_status",
"invites_issued", "invites_limit", "created_at",
).From("users").Where(sq.Eq{"email_hash": emailHash})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
user := &model.User{}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(
&user.ID, &user.Email, &user.EmailHash, &user.PasswordHash,
&user.Phone, &user.UserName, &user.CompanyName, &user.Balance,
&user.PaymentStatus, &user.InvitesIssued, &user.InvitesLimit, &user.CreatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.NewBusinessError(errs.UserNotFound, "user not found")
}
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to find user", err)
}
return user, nil
}
func (r *userRepository) FindByID(ctx context.Context, userID int) (*model.User, error) {
query := r.qb.Select(
"id", "email", "email_hash", "password_hash", "phone",
"user_name", "company_name", "balance", "payment_status",
"invites_issued", "invites_limit", "created_at",
).From("users").Where(sq.Eq{"id": userID})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
user := &model.User{}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(
&user.ID, &user.Email, &user.EmailHash, &user.PasswordHash,
&user.Phone, &user.UserName, &user.CompanyName, &user.Balance,
&user.PaymentStatus, &user.InvitesIssued, &user.InvitesLimit, &user.CreatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.NewBusinessError(errs.UserNotFound, "user not found")
}
if err != nil {
return nil, errs.NewInternalError(errs.DatabaseError, "failed to find user", err)
}
return user, nil
}
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
encryptedEmail, err := r.cryptoHelper.Encrypt(user.Email)
if err != nil {
return errs.NewInternalError(errs.EncryptionError, "failed to encrypt email", err)
}
encryptedPhone, err := r.cryptoHelper.Encrypt(user.Phone)
if err != nil {
return errs.NewInternalError(errs.EncryptionError, "failed to encrypt phone", err)
}
encryptedUserName, err := r.cryptoHelper.Encrypt(user.UserName)
if err != nil {
return errs.NewInternalError(errs.EncryptionError, "failed to encrypt user name", err)
}
query := r.qb.Insert("users").Columns(
"email", "email_hash", "password_hash", "phone", "user_name",
"company_name", "balance", "payment_status",
).Values(
encryptedEmail, user.EmailHash, user.PasswordHash, encryptedPhone,
encryptedUserName, user.CompanyName, user.Balance, user.PaymentStatus,
).Suffix("RETURNING id")
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&user.ID)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to create user", err)
}
return nil
}
func (r *userRepository) UpdateBalance(ctx context.Context, userID int, delta float64) error {
query := r.qb.Update("users").
Set("balance", sq.Expr("balance + ?", delta)).
Where(sq.Eq{"id": userID})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to update balance", err)
}
return nil
}
func (r *userRepository) GetBalance(ctx context.Context, userID int) (float64, error) {
query := r.qb.Select("balance").From("users").Where(sq.Eq{"id": userID})
sqlQuery, args, err := query.ToSql()
if err != nil {
return 0, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
var balance float64
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&balance)
if errors.Is(err, pgx.ErrNoRows) {
return 0, errs.NewBusinessError(errs.UserNotFound, "user not found")
}
if err != nil {
return 0, errs.NewInternalError(errs.DatabaseError, "failed to get balance", err)
}
return balance, nil
}
func (r *userRepository) IncrementInvitesIssued(ctx context.Context, userID int) error {
query := r.qb.Update("users").
Set("invites_issued", sq.Expr("invites_issued + 1")).
Where(sq.Eq{"id": userID})
sqlQuery, args, err := query.ToSql()
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
_, err = r.pool.Exec(ctx, sqlQuery, args...)
if err != nil {
return errs.NewInternalError(errs.DatabaseError, "failed to increment invites issued", err)
}
return nil
}
func (r *userRepository) CheckInviteLimit(ctx context.Context, userID int) (bool, error) {
query := r.qb.Select("invites_issued", "invites_limit").
From("users").
Where(sq.Eq{"id": userID}).
Suffix("FOR UPDATE")
sqlQuery, args, err := query.ToSql()
if err != nil {
return false, errs.NewInternalError(errs.DatabaseError, "failed to build query", err)
}
var issued, limit int
err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&issued, &limit)
if err != nil {
return false, errs.NewInternalError(errs.DatabaseError, "failed to check invite limit", err)
}
return issued < limit, nil
}

107
internal/service/auth.go Normal file
View File

@@ -0,0 +1,107 @@
package service
import (
"context"
"time"
"smart-search-back/internal/model"
"smart-search-back/internal/repository"
"smart-search-back/pkg/crypto"
"smart-search-back/pkg/errors"
"smart-search-back/pkg/jwt"
)
type authService struct {
userRepo repository.UserRepository
sessionRepo repository.SessionRepository
jwtSecret string
cryptoHelper *crypto.Crypto
}
func NewAuthService(userRepo repository.UserRepository, sessionRepo repository.SessionRepository, jwtSecret, cryptoSecret string) AuthService {
return &authService{
userRepo: userRepo,
sessionRepo: sessionRepo,
jwtSecret: jwtSecret,
cryptoHelper: crypto.NewCrypto(cryptoSecret),
}
}
func (s *authService) Login(ctx context.Context, email, password, ip, userAgent string) (accessToken, refreshToken string, err error) {
emailHash := s.cryptoHelper.EmailHash(email)
user, err := s.userRepo.FindByEmailHash(ctx, emailHash)
if err != nil {
return "", "", err
}
passwordHash := crypto.PasswordHash(password)
if user.PasswordHash != passwordHash {
return "", "", errors.NewBusinessError(errors.AuthInvalidCredentials, "Invalid email or password")
}
accessToken, err = jwt.GenerateAccessToken(user.ID, s.jwtSecret)
if err != nil {
return "", "", errors.NewInternalError(errors.InternalError, "failed to generate access token", err)
}
refreshToken, err = jwt.GenerateRefreshToken(user.ID, s.jwtSecret)
if err != nil {
return "", "", errors.NewInternalError(errors.InternalError, "failed to generate refresh token", err)
}
session := &model.Session{
UserID: user.ID,
AccessToken: accessToken,
RefreshToken: refreshToken,
IP: ip,
UserAgent: userAgent,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
}
if err := s.sessionRepo.Create(ctx, session); err != nil {
return "", "", err
}
return accessToken, refreshToken, nil
}
func (s *authService) Refresh(ctx context.Context, refreshToken string) (string, error) {
session, err := s.sessionRepo.FindByRefreshToken(ctx, refreshToken)
if err != nil {
return "", err
}
newAccessToken, err := jwt.GenerateAccessToken(session.UserID, s.jwtSecret)
if err != nil {
return "", errors.NewInternalError(errors.InternalError, "failed to generate access token", err)
}
if err := s.sessionRepo.UpdateAccessToken(ctx, refreshToken, newAccessToken); err != nil {
return "", err
}
return newAccessToken, nil
}
func (s *authService) Validate(ctx context.Context, accessToken string) (int, error) {
claims, err := jwt.ValidateToken(accessToken, s.jwtSecret)
if err != nil {
return 0, errors.NewBusinessError(errors.AuthInvalidToken, "Invalid or expired token")
}
if claims.Type != "access" {
return 0, errors.NewBusinessError(errors.AuthInvalidToken, "Token is not an access token")
}
userID, err := jwt.GetUserIDFromToken(accessToken, s.jwtSecret)
if err != nil {
return 0, errors.NewBusinessError(errors.AuthInvalidToken, "Invalid user ID in token")
}
return userID, nil
}
func (s *authService) Logout(ctx context.Context, refreshToken string) error {
return s.sessionRepo.Revoke(ctx, refreshToken)
}

View File

@@ -0,0 +1,38 @@
package service
import (
"context"
"smart-search-back/internal/model"
"github.com/google/uuid"
)
type AuthService interface {
Login(ctx context.Context, email, password, ip, userAgent string) (accessToken, refreshToken string, err error)
Refresh(ctx context.Context, refreshToken string) (string, error)
Validate(ctx context.Context, accessToken string) (int, error)
Logout(ctx context.Context, refreshToken string) error
}
type UserService interface {
GetInfo(ctx context.Context, userID int) (*UserInfo, error)
GetBalance(ctx context.Context, userID int) (float64, error)
GetStatistics(ctx context.Context, userID int) (*Statistics, error)
}
type InviteService interface {
Generate(ctx context.Context, userID, maxUses, ttlDays int) (*model.InviteCode, error)
GetInfo(ctx context.Context, code int64) (*model.InviteCode, error)
}
type RequestService interface {
CreateTZ(ctx context.Context, userID int, requestTxt string) (uuid.UUID, string, error)
ApproveTZ(ctx context.Context, requestID uuid.UUID, tzText string, userID int) ([]*model.Supplier, error)
GetMailingList(ctx context.Context, userID int) ([]*model.Request, error)
GetMailingListByID(ctx context.Context, requestID uuid.UUID) (*model.RequestDetail, error)
}
type SupplierService interface {
ExportExcel(ctx context.Context, requestID uuid.UUID) ([]byte, error)
}

View File

@@ -0,0 +1,57 @@
package service
import (
"context"
"math/rand"
"time"
"smart-search-back/internal/model"
"smart-search-back/internal/repository"
"smart-search-back/pkg/errors"
)
type inviteService struct {
inviteRepo repository.InviteRepository
userRepo repository.UserRepository
}
func NewInviteService(inviteRepo repository.InviteRepository, userRepo repository.UserRepository) InviteService {
return &inviteService{
inviteRepo: inviteRepo,
userRepo: userRepo,
}
}
func (s *inviteService) Generate(ctx context.Context, userID, maxUses, ttlDays int) (*model.InviteCode, error) {
canIssue, err := s.userRepo.CheckInviteLimit(ctx, userID)
if err != nil {
return nil, err
}
if !canIssue {
return nil, errors.NewBusinessError(errors.InviteLimitReached, "User reached maximum invite codes limit")
}
code := rand.Int63n(90000000) + 10000000
invite := &model.InviteCode{
UserID: userID,
Code: code,
CanBeUsedCount: maxUses,
ExpiresAt: time.Now().Add(time.Duration(ttlDays) * 24 * time.Hour),
}
if err := s.inviteRepo.Create(ctx, invite); err != nil {
return nil, err
}
if err := s.userRepo.IncrementInvitesIssued(ctx, userID); err != nil {
return nil, err
}
return invite, nil
}
func (s *inviteService) GetInfo(ctx context.Context, code int64) (*model.InviteCode, error) {
return s.inviteRepo.FindByCode(ctx, code)
}

154
internal/service/request.go Normal file
View File

@@ -0,0 +1,154 @@
package service
import (
"context"
"math"
"github.com/google/uuid"
"smart-search-back/internal/ai"
"smart-search-back/internal/model"
"smart-search-back/internal/repository"
"smart-search-back/pkg/errors"
)
type requestService struct {
requestRepo repository.RequestRepository
supplierRepo repository.SupplierRepository
tokenUsageRepo repository.TokenUsageRepository
userRepo repository.UserRepository
openAI *ai.OpenAIClient
perplexity *ai.PerplexityClient
}
func NewRequestService(
requestRepo repository.RequestRepository,
supplierRepo repository.SupplierRepository,
tokenUsageRepo repository.TokenUsageRepository,
userRepo repository.UserRepository,
openAI *ai.OpenAIClient,
perplexity *ai.PerplexityClient,
) RequestService {
return &requestService{
requestRepo: requestRepo,
supplierRepo: supplierRepo,
tokenUsageRepo: tokenUsageRepo,
userRepo: userRepo,
openAI: openAI,
perplexity: perplexity,
}
}
func (s *requestService) CreateTZ(ctx context.Context, userID int, requestTxt string) (uuid.UUID, string, error) {
req := &model.Request{
UserID: userID,
RequestTxt: requestTxt,
}
if err := s.requestRepo.Create(ctx, req); err != nil {
return uuid.Nil, "", err
}
if requestTxt == "" {
return req.ID, "", nil
}
tzText, err := s.openAI.GenerateTZ(requestTxt)
if err != nil {
if err := s.requestRepo.UpdateWithTZ(ctx, req.ID, "", false); err != nil {
return req.ID, "", err
}
return req.ID, "", err
}
inputLen := len(requestTxt)
outputLen := len(tzText)
promptTokens := 500
inputTokens := int(math.Ceil(float64(inputLen) / 2.0))
outputTokens := int(math.Ceil(float64(outputLen) / 2.0))
totalTokens := inputTokens + outputTokens + promptTokens
tokenPrice := 25000.0 / 1000000.0
cost := float64(totalTokens) * tokenPrice
tokenUsage := &model.TokenUsage{
RequestID: req.ID,
RequestTokenCount: inputTokens + promptTokens,
ResponseTokenCount: outputTokens,
TokenCost: cost,
Type: "tz",
}
if err := s.tokenUsageRepo.Create(ctx, tokenUsage); err != nil {
return req.ID, "", err
}
if err := s.userRepo.UpdateBalance(ctx, userID, -cost); err != nil {
return req.ID, "", err
}
if err := s.requestRepo.UpdateWithTZ(ctx, req.ID, tzText, true); err != nil {
return req.ID, "", err
}
return req.ID, tzText, nil
}
func (s *requestService) ApproveTZ(ctx context.Context, requestID uuid.UUID, tzText string, userID int) ([]*model.Supplier, error) {
if err := s.requestRepo.UpdateFinalTZ(ctx, requestID, tzText); err != nil {
return nil, err
}
var suppliers []*model.Supplier
var promptTokens, responseTokens int
var err error
for attempt := 0; attempt < 3; attempt++ {
suppliers, promptTokens, responseTokens, err = s.perplexity.FindSuppliers(tzText)
if err == nil {
break
}
}
if err != nil {
return nil, err
}
if len(suppliers) == 0 {
return nil, errors.NewInternalError(errors.AIAPIError, "no suppliers found", nil)
}
if err := s.supplierRepo.BulkInsert(ctx, requestID, suppliers); err != nil {
return nil, err
}
tokenPrice := 25000.0 / 1000000.0
totalTokens := promptTokens + responseTokens
cost := float64(totalTokens) * tokenPrice
tokenUsage := &model.TokenUsage{
RequestID: requestID,
RequestTokenCount: promptTokens,
ResponseTokenCount: responseTokens,
TokenCost: cost,
Type: "suppliers",
}
if err := s.tokenUsageRepo.Create(ctx, tokenUsage); err != nil {
return nil, err
}
if err := s.userRepo.UpdateBalance(ctx, userID, -cost); err != nil {
return nil, err
}
return suppliers, nil
}
func (s *requestService) GetMailingList(ctx context.Context, userID int) ([]*model.Request, error) {
return s.requestRepo.GetByUserID(ctx, userID)
}
func (s *requestService) GetMailingListByID(ctx context.Context, requestID uuid.UUID) (*model.RequestDetail, error) {
return s.requestRepo.GetDetailByID(ctx, requestID)
}

View File

@@ -0,0 +1,75 @@
package service
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/xuri/excelize/v2"
"smart-search-back/internal/repository"
)
type supplierService struct {
supplierRepo repository.SupplierRepository
}
func NewSupplierService(supplierRepo repository.SupplierRepository) SupplierService {
return &supplierService{
supplierRepo: supplierRepo,
}
}
func (s *supplierService) ExportExcel(ctx context.Context, requestID uuid.UUID) ([]byte, error) {
suppliers, err := s.supplierRepo.GetByRequestID(ctx, requestID)
if err != nil {
return nil, err
}
f := excelize.NewFile()
defer f.Close()
sheetName := "Suppliers"
index, err := f.NewSheet(sheetName)
if err != nil {
return nil, err
}
headers := []string{"Company ID", "Email", "Phone", "Company Name", "URL"}
for i, header := range headers {
cell := fmt.Sprintf("%c1", 'A'+i)
f.SetCellValue(sheetName, cell, header)
}
style, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0E0E0"}, Pattern: 1},
})
if err == nil {
f.SetCellStyle(sheetName, "A1", fmt.Sprintf("%c1", 'A'+len(headers)-1), style)
}
for i, supplier := range suppliers {
row := i + 2
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), supplier.ID)
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), supplier.Email)
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), supplier.Phone)
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), supplier.Name)
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), supplier.URL)
}
f.SetColWidth(sheetName, "A", "A", 12)
f.SetColWidth(sheetName, "B", "B", 30)
f.SetColWidth(sheetName, "C", "C", 20)
f.SetColWidth(sheetName, "D", "D", 40)
f.SetColWidth(sheetName, "E", "E", 40)
f.SetActiveSheet(index)
f.DeleteSheet("Sheet1")
buffer, err := f.WriteToBuffer()
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}

View File

@@ -0,0 +1,449 @@
package tests
import (
"context"
"errors"
"testing"
"time"
"github.com/gojuno/minimock/v3"
"github.com/stretchr/testify/suite"
"smart-search-back/internal/mocks"
"smart-search-back/internal/model"
"smart-search-back/internal/service"
"smart-search-back/pkg/crypto"
apperrors "smart-search-back/pkg/errors"
"smart-search-back/pkg/jwt"
)
type Suite struct {
suite.Suite
ctx context.Context
authService service.AuthService
userRepo *mocks.UserRepositoryMock
sessionRepo *mocks.SessionRepositoryMock
}
func newSuite(ctx context.Context) *Suite {
return &Suite{ctx: ctx}
}
func TestAuthService(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
suite.Run(t, newSuite(ctx))
}
func (s *Suite) SetupSuite() {
s.ctx = context.Background()
}
func (s *Suite) SetupTest() {
ctrl := minimock.NewController(s.T())
s.userRepo = mocks.NewUserRepositoryMock(ctrl)
s.sessionRepo = mocks.NewSessionRepositoryMock(ctrl)
s.authService = service.NewAuthService(s.userRepo, s.sessionRepo)
}
func createTestUser(password string) *model.User {
return &model.User{
ID: 1,
Email: "test@example.com",
EmailHash: crypto.EmailHash("test@example.com"),
PasswordHash: crypto.PasswordHash(password),
CreatedAt: time.Now(),
}
}
func createTestSession(userID int) *model.Session {
return &model.Session{
ID: 1,
UserID: userID,
AccessToken: "test-access-token",
RefreshToken: "test-refresh-token",
IP: "127.0.0.1",
UserAgent: "test-agent",
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
}
}
func (s *Suite) TestAuthService_Login_Success() {
password := "testpassword"
user := createTestUser(password)
s.userRepo.FindByEmailHashMock.Return(user, nil)
s.sessionRepo.CreateMock.Return(nil)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
"test@example.com",
password,
"127.0.0.1",
"test-agent",
)
s.NoError(err)
s.NotEmpty(accessToken)
s.NotEmpty(refreshToken)
}
func (s *Suite) TestAuthService_Login_UserNotFound() {
err := apperrors.NewBusinessError(apperrors.UserNotFound, "user not found")
s.userRepo.FindByEmailHashMock.Return(nil, err)
accessToken, refreshToken, loginErr := s.authService.Login(
s.ctx,
"test@example.com",
"password",
"127.0.0.1",
"test-agent",
)
s.Error(loginErr)
s.Empty(accessToken)
s.Empty(refreshToken)
var appErr *apperrors.AppError
s.True(errors.As(loginErr, &appErr))
s.Equal(apperrors.UserNotFound, appErr.Code)
}
func (s *Suite) TestAuthService_Login_DatabaseError_OnFindUser() {
dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to find user", nil)
s.userRepo.FindByEmailHashMock.Return(nil, dbErr)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
"test@example.com",
"password",
"127.0.0.1",
"test-agent",
)
s.Error(err)
s.Empty(accessToken)
s.Empty(refreshToken)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.InternalErrorType, appErr.Type)
}
func (s *Suite) TestAuthService_Login_InvalidPassword() {
password := "correctpassword"
user := createTestUser(password)
s.userRepo.FindByEmailHashMock.Return(user, nil)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
"test@example.com",
"wrongpassword",
"127.0.0.1",
"test-agent",
)
s.Error(err)
s.Empty(accessToken)
s.Empty(refreshToken)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.AuthInvalidCredentials, appErr.Code)
s.Contains(appErr.Message, "Invalid email or password")
}
func (s *Suite) TestAuthService_Login_EmptyPassword() {
user := createTestUser("")
s.userRepo.FindByEmailHashMock.Return(user, nil)
s.sessionRepo.CreateMock.Return(nil)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
"test@example.com",
"",
"127.0.0.1",
"test-agent",
)
s.NoError(err)
s.NotEmpty(accessToken)
s.NotEmpty(refreshToken)
}
func (s *Suite) TestAuthService_Login_EmailWithSpacesAndCase() {
password := "testpassword"
normalizedEmail := "test@example.com"
user := createTestUser(password)
user.EmailHash = crypto.EmailHash(normalizedEmail)
s.userRepo.FindByEmailHashMock.Return(user, nil)
s.sessionRepo.CreateMock.Return(nil)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
" TEST@EXAMPLE.COM ",
password,
"127.0.0.1",
"test-agent",
)
s.NoError(err)
s.NotEmpty(accessToken)
s.NotEmpty(refreshToken)
}
func (s *Suite) TestAuthService_Login_EmptyEmail() {
err := apperrors.NewBusinessError(apperrors.UserNotFound, "user not found")
s.userRepo.FindByEmailHashMock.Return(nil, err)
accessToken, refreshToken, loginErr := s.authService.Login(
s.ctx,
"",
"password",
"127.0.0.1",
"test-agent",
)
s.Error(loginErr)
s.Empty(accessToken)
s.Empty(refreshToken)
}
func (s *Suite) TestAuthService_Login_SessionCreateError() {
password := "testpassword"
user := createTestUser(password)
s.userRepo.FindByEmailHashMock.Return(user, nil)
dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to create session", nil)
s.sessionRepo.CreateMock.Return(dbErr)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
"test@example.com",
password,
"127.0.0.1",
"test-agent",
)
s.Error(err)
s.Empty(accessToken)
s.Empty(refreshToken)
}
func (s *Suite) TestAuthService_Login_EmptyIPAndUserAgent() {
password := "testpassword"
user := createTestUser(password)
s.userRepo.FindByEmailHashMock.Return(user, nil)
s.sessionRepo.CreateMock.Return(nil)
accessToken, refreshToken, err := s.authService.Login(
s.ctx,
"test@example.com",
password,
"",
"",
)
s.NoError(err)
s.NotEmpty(accessToken)
s.NotEmpty(refreshToken)
}
func (s *Suite) TestAuthService_Refresh_Success() {
session := createTestSession(1)
s.sessionRepo.FindByRefreshTokenMock.Return(session, nil)
s.sessionRepo.UpdateAccessTokenMock.Return(nil)
accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token")
s.NoError(err)
s.NotEmpty(accessToken)
}
func (s *Suite) TestAuthService_Refresh_RefreshInvalid() {
err := apperrors.NewBusinessError(apperrors.RefreshInvalid, "refresh token is invalid or expired")
s.sessionRepo.FindByRefreshTokenMock.Return(nil, err)
accessToken, refreshErr := s.authService.Refresh(s.ctx, "invalid-token")
s.Error(refreshErr)
s.Empty(accessToken)
var appErr *apperrors.AppError
s.True(errors.As(refreshErr, &appErr))
s.Equal(apperrors.RefreshInvalid, appErr.Code)
}
func (s *Suite) TestAuthService_Refresh_DatabaseError_OnFindSession() {
dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to find session", nil)
s.sessionRepo.FindByRefreshTokenMock.Return(nil, dbErr)
accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token")
s.Error(err)
s.Empty(accessToken)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.InternalErrorType, appErr.Type)
}
func (s *Suite) TestAuthService_Refresh_EmptyToken() {
err := apperrors.NewBusinessError(apperrors.RefreshInvalid, "refresh token is invalid or expired")
s.sessionRepo.FindByRefreshTokenMock.Return(nil, err)
accessToken, refreshErr := s.authService.Refresh(s.ctx, "")
s.Error(refreshErr)
s.Empty(accessToken)
}
func (s *Suite) TestAuthService_Refresh_UpdateAccessTokenError() {
session := createTestSession(1)
s.sessionRepo.FindByRefreshTokenMock.Return(session, nil)
dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to update access token", nil)
s.sessionRepo.UpdateAccessTokenMock.Return(dbErr)
accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token")
s.Error(err)
s.Empty(accessToken)
}
func (s *Suite) TestAuthService_Refresh_UserIDZero() {
session := createTestSession(0)
s.sessionRepo.FindByRefreshTokenMock.Return(session, nil)
s.sessionRepo.UpdateAccessTokenMock.Return(nil)
accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token")
s.NoError(err)
s.NotEmpty(accessToken)
}
func (s *Suite) TestAuthService_Validate_Success() {
s.T().Parallel()
userID := 1
accessToken, err := jwt.GenerateAccessToken(userID)
s.NoError(err)
validatedUserID, validateErr := s.authService.Validate(s.ctx, accessToken)
s.NoError(validateErr)
s.Equal(userID, validatedUserID)
}
func (s *Suite) TestAuthService_Validate_EmptyToken() {
s.T().Parallel()
userID, err := s.authService.Validate(s.ctx, "")
s.Error(err)
s.Equal(0, userID)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.AuthInvalidToken, appErr.Code)
}
func (s *Suite) TestAuthService_Validate_InvalidTokenFormat() {
s.T().Parallel()
userID, err := s.authService.Validate(s.ctx, "invalid.token.format")
s.Error(err)
s.Equal(0, userID)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.AuthInvalidToken, appErr.Code)
}
func (s *Suite) TestAuthService_Validate_RefreshTokenInsteadOfAccess() {
s.T().Parallel()
userID := 1
refreshToken, err := jwt.GenerateRefreshToken(userID)
s.NoError(err)
validatedUserID, validateErr := s.authService.Validate(s.ctx, refreshToken)
s.Error(validateErr)
s.Equal(0, validatedUserID)
var appErr *apperrors.AppError
s.True(errors.As(validateErr, &appErr))
s.Equal(apperrors.AuthInvalidToken, appErr.Code)
s.Contains(appErr.Message, "not an access token")
}
func (s *Suite) TestAuthService_Validate_UserIDZero() {
s.T().Parallel()
accessToken, err := jwt.GenerateAccessToken(0)
s.NoError(err)
validatedUserID, validateErr := s.authService.Validate(s.ctx, accessToken)
s.NoError(validateErr)
s.Equal(0, validatedUserID)
}
func (s *Suite) TestAuthService_Validate_InvalidSignature() {
s.T().Parallel()
invalidToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTYwOTQ1NjgwMCwiZXhwIjoxNjA5NDY1ODAwfQ.invalid-signature"
userID, err := s.authService.Validate(s.ctx, invalidToken)
s.Error(err)
s.Equal(0, userID)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.AuthInvalidToken, appErr.Code)
}
func (s *Suite) TestAuthService_Logout_Success() {
s.sessionRepo.RevokeMock.Return(nil)
err := s.authService.Logout(s.ctx, "test-refresh-token")
s.NoError(err)
}
func (s *Suite) TestAuthService_Logout_DatabaseError() {
dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to revoke session", nil)
s.sessionRepo.RevokeMock.Return(dbErr)
err := s.authService.Logout(s.ctx, "test-refresh-token")
s.Error(err)
var appErr *apperrors.AppError
s.True(errors.As(err, &appErr))
s.Equal(apperrors.InternalErrorType, appErr.Type)
}
func (s *Suite) TestAuthService_Logout_EmptyToken() {
s.sessionRepo.RevokeMock.Return(nil)
err := s.authService.Logout(s.ctx, "")
s.NoError(err)
}
func (s *Suite) TestAuthService_Logout_NonExistentToken() {
s.sessionRepo.RevokeMock.Return(nil)
err := s.authService.Logout(s.ctx, "non-existent-token")
s.NoError(err)
}

83
internal/service/user.go Normal file
View File

@@ -0,0 +1,83 @@
package service
import (
"context"
"smart-search-back/internal/repository"
"smart-search-back/pkg/crypto"
)
type userService struct {
userRepo repository.UserRepository
requestRepo repository.RequestRepository
cryptoHelper *crypto.Crypto
}
type UserInfo struct {
Email string
Name string
Phone string
CompanyName string
PaymentStatus string
}
type Statistics struct {
RequestsCount int
SuppliersCount int
CreatedTZ int
}
func NewUserService(userRepo repository.UserRepository, requestRepo repository.RequestRepository, cryptoSecret string) UserService {
return &userService{
userRepo: userRepo,
requestRepo: requestRepo,
cryptoHelper: crypto.NewCrypto(cryptoSecret),
}
}
func (s *userService) GetInfo(ctx context.Context, userID int) (*UserInfo, error) {
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return nil, err
}
email, err := s.cryptoHelper.Decrypt(user.Email)
if err != nil {
return nil, err
}
phone, err := s.cryptoHelper.Decrypt(user.Phone)
if err != nil {
return nil, err
}
userName, err := s.cryptoHelper.Decrypt(user.UserName)
if err != nil {
return nil, err
}
return &UserInfo{
Email: email,
Name: userName,
Phone: phone,
CompanyName: user.CompanyName,
PaymentStatus: user.PaymentStatus,
}, nil
}
func (s *userService) GetBalance(ctx context.Context, userID int) (float64, error) {
return s.userRepo.GetBalance(ctx, userID)
}
func (s *userService) GetStatistics(ctx context.Context, userID int) (*Statistics, error) {
requestsCount, suppliersCount, createdTZ, err := s.requestRepo.GetUserStatistics(ctx, userID)
if err != nil {
return nil, err
}
return &Statistics{
RequestsCount: requestsCount,
SuppliersCount: suppliersCount,
CreatedTZ: createdTZ,
}, nil
}

View File

@@ -0,0 +1,69 @@
package worker
import (
"context"
"log"
"time"
"smart-search-back/internal/repository"
)
type InviteCleaner struct {
inviteRepo repository.InviteRepository
ctx context.Context
ticker *time.Ticker
done chan bool
}
func NewInviteCleaner(ctx context.Context, inviteRepo repository.InviteRepository) *InviteCleaner {
return &InviteCleaner{
inviteRepo: inviteRepo,
ctx: ctx,
done: make(chan bool),
}
}
func (w *InviteCleaner) Start() {
w.ticker = time.NewTicker(6 * time.Hour)
w.deactivateExpiredInvites()
go func() {
for {
select {
case <-w.ticker.C:
w.deactivateExpiredInvites()
case <-w.done:
return
case <-w.ctx.Done():
log.Println("Invite cleaner context cancelled, stopping worker")
return
}
}
}()
log.Println("Invite cleaner worker started (runs every 6 hours)")
}
func (w *InviteCleaner) Stop() {
if w.ticker != nil {
w.ticker.Stop()
}
select {
case w.done <- true:
default:
}
log.Println("Invite cleaner worker stopped")
}
func (w *InviteCleaner) deactivateExpiredInvites() {
count, err := w.inviteRepo.DeactivateExpired(w.ctx)
if err != nil {
log.Printf("Error deactivating expired invites: %v", err)
return
}
if count > 0 {
log.Printf("Deactivated %d expired invite codes", count)
}
}

View File

@@ -0,0 +1,69 @@
package worker
import (
"context"
"log"
"time"
"smart-search-back/internal/repository"
)
type SessionCleaner struct {
sessionRepo repository.SessionRepository
ctx context.Context
ticker *time.Ticker
done chan bool
}
func NewSessionCleaner(ctx context.Context, sessionRepo repository.SessionRepository) *SessionCleaner {
return &SessionCleaner{
sessionRepo: sessionRepo,
ctx: ctx,
done: make(chan bool),
}
}
func (w *SessionCleaner) Start() {
w.ticker = time.NewTicker(1 * time.Hour)
w.cleanExpiredSessions()
go func() {
for {
select {
case <-w.ticker.C:
w.cleanExpiredSessions()
case <-w.done:
return
case <-w.ctx.Done():
log.Println("Session cleaner context cancelled, stopping worker")
return
}
}
}()
log.Println("Session cleaner worker started (runs every hour)")
}
func (w *SessionCleaner) Stop() {
if w.ticker != nil {
w.ticker.Stop()
}
select {
case w.done <- true:
default:
}
log.Println("Session cleaner worker stopped")
}
func (w *SessionCleaner) cleanExpiredSessions() {
count, err := w.sessionRepo.DeleteExpired(w.ctx)
if err != nil {
log.Printf("Error cleaning expired sessions: %v", err)
return
}
if count > 0 {
log.Printf("Cleaned %d expired sessions", count)
}
}