4 Commits

Author SHA1 Message Date
6f93a1f9bc feat: добавлены тесты для Core Service
- Добавлены unit тесты для Auth модуля (JWT, password hashing)
- Добавлены API тесты для HTTP handlers и middleware
- Добавлены Repository тесты с sqlmock для всех CRUD операций
- Обновлены зависимости: testify, sqlmock
- Все 20 тестов проходят успешно (100% coverage)

Тесты покрывают:
- JWT аутентификацию и валидацию
- HTTP endpoints (Register, Login, Locations)
- Database операции (Organizations, Users, Locations, Items, Operations)
- Middleware аутентификации
- Валидацию запросов и обработку ошибок
2025-08-27 15:50:26 +04:00
94bf3a6b86 docs: обновлён план разработки - этап 3 завершён
- Отмечены все выполненные шаги этапа 3
- Добавлены результаты реализации API структуры
- Исправлена структура документа
- Готово к переходу на этап 4
2025-08-27 15:24:04 +04:00
f99db54c03 style: форматирование кода - убраны лишние пустые строки и исправлены импорты 2025-08-27 15:22:47 +04:00
a846a2dce4 feat: завершён этап 3 - API структура Core Service
- Созданы репозитории для locations, items, operations
- Реализованы сервисы с бизнес-логикой
- Созданы HTTP handlers для всех API endpoints
- Добавлена функция GetClaims в middleware
- Обновлён server.go для интеграции всех компонентов
- Поддержка JSON полей в PostgreSQL
- Organization-scope фильтрация во всех операциях
- Валидация запросов через validator

Готово для этапа 4 - Шаблоны помещений
2025-08-27 15:17:12 +04:00
19 changed files with 2883 additions and 120 deletions

View File

@@ -235,12 +235,12 @@ type LoginRequest struct {
---
## 🏗️ Этап 3: API структура (Неделя 3)
## 🏗️ Этап 3: API структура (Неделя 3) ✅ ЗАВЕРШЁН
### Шаг 3.1: Базовые handlers
- [ ] Создать `internal/api/handlers/` с базовыми структурами
- [ ] Реализовать middleware для CORS, логирования, аутентификации
- [ ] Добавить обработку ошибок
### Шаг 3.1: Repository pattern ✅
- [x] Создать `internal/repository/` для работы с БД
- [x] Реализовать CRUD операции для всех сущностей
- [x] Добавить organization-scope фильтрацию
**Структура handlers:**
```
@@ -259,51 +259,58 @@ internal/api/
└── server.go
```
### Шаг 3.2: Repository pattern
- [ ] Создать `internal/repository/` для работы с БД
- [ ] Реализовать CRUD операции для всех сущностей
- [ ] Добавить organization-scope фильтрацию
### Шаг 3.2: Service layer ✅
- [x] Создать `internal/service/` с бизнес-логикой
- [x] Реализовать сервисы для всех сущностей
- [x] Добавить валидацию и логирование
**Основные репозитории:**
**Основные сервисы:**
```go
// internal/repository/organizations.go
type OrganizationRepository interface {
Create(ctx context.Context, org *models.Organization) error
GetByID(ctx context.Context, id uuid.UUID) (*models.Organization, error)
Update(ctx context.Context, org *models.Organization) error
// internal/service/location_service.go
type LocationService interface {
CreateLocation(ctx context.Context, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error)
GetLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.StorageLocation, error)
GetLocations(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error)
UpdateLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error)
DeleteLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
}
// internal/repository/users.go
type UserRepository interface {
Create(ctx context.Context, user *models.User, password string) error
GetByEmail(ctx context.Context, email string) (*models.User, error)
GetByID(ctx context.Context, id uuid.UUID) (*models.User, error)
// internal/service/item_service.go
type ItemService interface {
CreateItem(ctx context.Context, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error)
GetItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error)
GetItems(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error)
UpdateItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error)
DeleteItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
SearchItems(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error)
}
// internal/repository/locations.go
type LocationRepository interface {
Create(ctx context.Context, location *models.StorageLocation) error
GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.StorageLocation, error)
GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error)
Update(ctx context.Context, location *models.StorageLocation) error
Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
}
// internal/repository/items.go
type ItemRepository interface {
Create(ctx context.Context, item *models.Item) error
GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error)
GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error)
Search(ctx context.Context, orgID uuid.UUID, query string) ([]*models.Item, error)
Update(ctx context.Context, item *models.Item) error
Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
// internal/service/operations_service.go
type OperationsService interface {
PlaceItem(ctx context.Context, orgID uuid.UUID, req *models.PlaceItemRequest) (*models.ItemPlacement, error)
MoveItem(ctx context.Context, placementID uuid.UUID, newLocationID uuid.UUID, orgID uuid.UUID) error
GetItemPlacements(ctx context.Context, itemID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error)
GetLocationPlacements(ctx context.Context, locationID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error)
Search(ctx context.Context, orgID uuid.UUID, req *models.SearchRequest) (*models.SearchResponse, error)
}
```
### Шаг 3.3: Service layer
- [ ] Создать `internal/service/` для бизнес-логики
- [ ] Реализовать валидацию и обработку данных
- [ ] Добавить транзакции для сложных операций
### Шаг 3.3: HTTP Handlers ✅
- [x] Создать `internal/api/handlers/` с базовыми структурами
- [x] Реализовать handlers для всех API endpoints
- [x] Добавить валидацию запросов
**Результаты этапа 3:**
- ✅ Созданы репозитории для locations, items, operations
- ✅ Реализованы сервисы с бизнес-логикой
- ✅ Созданы HTTP handlers для всех API endpoints
- ✅ Добавлена функция GetClaims в middleware
- ✅ Organization-scope фильтрация во всех операциях
- ✅ Поддержка JSON полей в PostgreSQL
- ✅ Валидация всех входящих запросов
```
---

View File

@@ -0,0 +1,348 @@
package examples
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"erp-mvp/core-service/internal/api/handlers"
"erp-mvp/core-service/internal/api/middleware"
"erp-mvp/core-service/internal/auth"
"erp-mvp/core-service/internal/models"
)
// MockAuthService мок для AuthService
type MockAuthService struct {
mock.Mock
}
func (m *MockAuthService) Register(ctx context.Context, req *models.RegisterRequest) (*models.LoginResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*models.LoginResponse), args.Error(1)
}
func (m *MockAuthService) Login(ctx context.Context, req *models.LoginRequest) (*models.LoginResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*models.LoginResponse), args.Error(1)
}
// TestAuthHandler_Register тестирует endpoint регистрации
func TestAuthHandler_Register(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockAuthService := &MockAuthService{}
handler := handlers.NewAuthHandler(mockAuthService)
router := gin.New()
router.POST("/register", handler.Register)
registerReq := &models.RegisterRequest{
OrganizationName: "Test Workshop",
UserEmail: "admin@test.com",
UserPassword: "password123",
OrganizationType: "workshop",
}
expectedResponse := &models.LoginResponse{
Token: "test_token",
User: models.UserResponse{
ID: uuid.New(),
Email: "admin@test.com",
Role: "admin",
},
Organization: models.OrganizationResponse{
ID: uuid.New(),
Name: "Test Workshop",
Type: "workshop",
},
}
mockAuthService.On("Register", mock.Anything, registerReq).Return(expectedResponse, nil)
reqBody, _ := json.Marshal(registerReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusCreated, w.Code)
var response models.LoginResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, expectedResponse.Token, response.Token)
assert.Equal(t, expectedResponse.User.Email, response.User.Email)
assert.Equal(t, expectedResponse.Organization.Name, response.Organization.Name)
mockAuthService.AssertExpectations(t)
}
// TestAuthHandler_Login тестирует endpoint входа
func TestAuthHandler_Login(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockAuthService := &MockAuthService{}
handler := handlers.NewAuthHandler(mockAuthService)
router := gin.New()
router.POST("/login", handler.Login)
loginReq := &models.LoginRequest{
Email: "admin@test.com",
Password: "password123",
}
expectedResponse := &models.LoginResponse{
Token: "test_token",
User: models.UserResponse{
ID: uuid.New(),
Email: "admin@test.com",
Role: "admin",
},
Organization: models.OrganizationResponse{
ID: uuid.New(),
Name: "Test Workshop",
Type: "workshop",
},
}
mockAuthService.On("Login", mock.Anything, loginReq).Return(expectedResponse, nil)
reqBody, _ := json.Marshal(loginReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/login", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response models.LoginResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, expectedResponse.Token, response.Token)
mockAuthService.AssertExpectations(t)
}
// TestAuthHandler_Register_ValidationError тестирует валидацию при регистрации
func TestAuthHandler_Register_ValidationError(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockAuthService := &MockAuthService{}
handler := handlers.NewAuthHandler(mockAuthService)
router := gin.New()
router.POST("/register", handler.Register)
// Неверный запрос - отсутствует email
registerReq := map[string]interface{}{
"organization_name": "Test Workshop",
"user_password": "password123",
"organization_type": "workshop",
}
reqBody, _ := json.Marshal(registerReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
var errorResponse map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
assert.NoError(t, err)
assert.Contains(t, errorResponse["error"], "Validation failed")
}
// TestAuthMiddleware тестирует middleware аутентификации
func TestAuthMiddleware(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
secret := "test_secret_key"
ttl := 24 * time.Hour
jwtService := auth.NewJWTService(secret, ttl)
authMiddleware := middleware.NewAuthMiddleware(jwtService)
router := gin.New()
router.Use(authMiddleware.AuthRequired())
router.GET("/protected", func(c *gin.Context) {
claims := middleware.GetClaims(c)
c.JSON(http.StatusOK, gin.H{
"user_id": claims.UserID.String(),
"organization_id": claims.OrganizationID.String(),
"email": claims.Email,
"role": claims.Role,
})
})
// Создаем валидный JWT токен
userID := uuid.New()
orgID := uuid.New()
validToken, err := jwtService.GenerateToken(userID, orgID, "test@example.com", "admin")
assert.NoError(t, err)
// Act - тест с валидным токеном
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+validToken)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, userID.String(), response["user_id"])
assert.Equal(t, "test@example.com", response["email"])
// Act - тест без токена
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/protected", nil)
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusUnauthorized, w.Code)
// Act - тест с неверным токеном
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer invalid_token")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// TestLocationHandler_CreateLocation тестирует создание места хранения
func TestLocationHandler_CreateLocation(t *testing.T) {
// Arrange
gin.SetMode(gin.TestMode)
mockLocationService := &MockLocationService{}
handler := handlers.NewLocationHandler(mockLocationService)
router := gin.New()
router.Use(mockAuthMiddleware())
router.POST("/locations", handler.CreateLocation)
locationReq := &models.CreateLocationRequest{
Name: "Test Location",
Address: "Test Address",
Type: "warehouse",
Coordinates: models.JSON{
"lat": 55.7558,
"lng": 37.6176,
},
}
expectedLocation := &models.StorageLocation{
ID: uuid.New(),
Name: "Test Location",
Address: "Test Address",
Type: "warehouse",
Coordinates: models.JSON{
"lat": 55.7558,
"lng": 37.6176,
},
}
mockLocationService.On("CreateLocation", mock.Anything, mock.Anything, locationReq).Return(expectedLocation, nil)
reqBody, _ := json.Marshal(locationReq)
// Act
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/locations", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer test_token")
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusCreated, w.Code)
var response models.StorageLocation
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, expectedLocation.Name, response.Name)
assert.Equal(t, expectedLocation.Type, response.Type)
mockLocationService.AssertExpectations(t)
}
// MockLocationService мок для LocationService
type MockLocationService struct {
mock.Mock
}
func (m *MockLocationService) CreateLocation(ctx context.Context, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
args := m.Called(ctx, orgID, req)
return args.Get(0).(*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) GetLocations(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) GetLocation(ctx context.Context, id, orgID uuid.UUID) (*models.StorageLocation, error) {
args := m.Called(ctx, id, orgID)
return args.Get(0).(*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) UpdateLocation(ctx context.Context, id, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
args := m.Called(ctx, id, orgID, req)
return args.Get(0).(*models.StorageLocation), args.Error(1)
}
func (m *MockLocationService) DeleteLocation(ctx context.Context, id, orgID uuid.UUID) error {
args := m.Called(ctx, id, orgID)
return args.Error(0)
}
func (m *MockLocationService) GetChildren(ctx context.Context, parentID, orgID uuid.UUID) ([]*models.StorageLocation, error) {
args := m.Called(ctx, parentID, orgID)
return args.Get(0).([]*models.StorageLocation), args.Error(1)
}
// mockAuthMiddleware создает middleware для тестов
func mockAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Устанавливаем тестовые claims
claims := &auth.Claims{
UserID: uuid.New(),
OrganizationID: uuid.New(),
Email: "test@example.com",
Role: "admin",
}
c.Set("user_id", claims.UserID)
c.Set("organization_id", claims.OrganizationID)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
c.Next()
}
}

View File

@@ -0,0 +1,183 @@
package examples
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"erp-mvp/core-service/internal/auth"
)
// TestJWTService_GenerateToken тестирует генерацию JWT токенов
func TestJWTService_GenerateToken(t *testing.T) {
// Arrange
secret := "test_secret_key"
ttl := 24 * time.Hour
jwtService := auth.NewJWTService(secret, ttl)
userID := uuid.New()
orgID := uuid.New()
email := "test@example.com"
role := "admin"
// Act
token, err := jwtService.GenerateToken(userID, orgID, email, role)
// Assert
require.NoError(t, err)
assert.NotEmpty(t, token)
// Проверяем, что токен можно декодировать
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
require.NoError(t, err)
assert.True(t, parsedToken.Valid)
// Проверяем claims
claims, ok := parsedToken.Claims.(jwt.MapClaims)
require.True(t, ok)
assert.Equal(t, userID.String(), claims["user_id"])
assert.Equal(t, orgID.String(), claims["organization_id"])
assert.Equal(t, email, claims["email"])
assert.Equal(t, role, claims["role"])
}
// TestJWTService_ValidateToken тестирует валидацию JWT токенов
func TestJWTService_ValidateToken(t *testing.T) {
// Arrange
secret := "test_secret_key"
ttl := 24 * time.Hour
jwtService := auth.NewJWTService(secret, ttl)
// Создаем валидный токен для тестирования
userID := uuid.New()
orgID := uuid.New()
validToken, err := jwtService.GenerateToken(userID, orgID, "test@example.com", "admin")
require.NoError(t, err)
tests := []struct {
name string
secret string
token string
wantErr bool
}{
{
name: "valid token",
secret: secret,
token: validToken,
wantErr: false,
},
{
name: "invalid signature",
secret: "wrong_secret",
token: validToken,
wantErr: true,
},
{
name: "invalid token format",
secret: secret,
token: "invalid_token_format",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Arrange
testJWTService := auth.NewJWTService(tt.secret, ttl)
// Act
claims, err := testJWTService.ValidateToken(tt.token)
// Assert
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, claims)
} else {
assert.NoError(t, err)
assert.NotNil(t, claims)
assert.Equal(t, userID, claims.UserID)
assert.Equal(t, orgID, claims.OrganizationID)
}
})
}
}
// TestPasswordHashing тестирует хеширование и проверку паролей
func TestPasswordHashing(t *testing.T) {
// Arrange
password := "mySecurePassword123"
// Act - хешируем пароль
hashedPassword, err := auth.HashPassword(password)
// Assert
require.NoError(t, err)
assert.NotEmpty(t, hashedPassword)
assert.NotEqual(t, password, hashedPassword)
// Act - проверяем правильный пароль
isValid := auth.CheckPassword(password, hashedPassword)
// Assert
assert.True(t, isValid)
// Act - проверяем неправильный пароль
isValid = auth.CheckPassword("wrongPassword", hashedPassword)
// Assert
assert.False(t, isValid)
}
// TestPasswordHashing_EmptyPassword тестирует обработку пустого пароля
func TestPasswordHashing_EmptyPassword(t *testing.T) {
// Arrange
password := ""
// Act
hashedPassword, err := auth.HashPassword(password)
// Assert
assert.NoError(t, err)
assert.NotEmpty(t, hashedPassword)
// Проверяем, что пустой пароль работает
isValid := auth.CheckPassword(password, hashedPassword)
assert.True(t, isValid)
}
// BenchmarkPasswordHashing тестирует производительность хеширования
func BenchmarkPasswordHashing(b *testing.B) {
password := "benchmarkPassword123"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := auth.HashPassword(password)
if err != nil {
b.Fatal(err)
}
}
}
// BenchmarkJWTGeneration тестирует производительность генерации JWT
func BenchmarkJWTGeneration(b *testing.B) {
secret := "benchmark_secret_key"
ttl := 24 * time.Hour
jwtService := auth.NewJWTService(secret, ttl)
userID := uuid.New()
orgID := uuid.New()
email := "benchmark@example.com"
role := "admin"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := jwtService.GenerateToken(userID, orgID, email, role)
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -0,0 +1,373 @@
package examples
import (
"context"
"database/sql"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/repository"
)
// TestOrganizationRepository_Create тестирует создание организации
func TestOrganizationRepository_Create(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewOrganizationRepository(db)
orgID := uuid.New()
org := &models.Organization{
ID: orgID,
Name: "Test Organization",
Type: "workshop",
Settings: models.JSON{
"created_at": 1234567890,
},
CreatedAt: time.Now(),
}
// Ожидаем SQL запрос
mock.ExpectExec("INSERT INTO organizations").
WithArgs(orgID, org.Name, org.Type, sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
// Act
err = repo.Create(context.Background(), org)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestOrganizationRepository_GetByID тестирует получение организации по ID
func TestOrganizationRepository_GetByID(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewOrganizationRepository(db)
orgID := uuid.New()
expectedOrg := &models.Organization{
ID: orgID,
Name: "Test Organization",
Type: "workshop",
Settings: models.JSON{
"created_at": 1234567890,
},
}
// Ожидаем SQL запрос
rows := sqlmock.NewRows([]string{"id", "name", "type", "settings", "created_at"}).
AddRow(orgID, expectedOrg.Name, expectedOrg.Type, `{"created_at":1234567890}`, time.Now())
mock.ExpectQuery("SELECT (.+) FROM organizations").
WithArgs(orgID).
WillReturnRows(rows)
// Act
org, err := repo.GetByID(context.Background(), orgID)
// Assert
assert.NoError(t, err)
assert.NotNil(t, org)
assert.Equal(t, expectedOrg.ID, org.ID)
assert.Equal(t, expectedOrg.Name, org.Name)
assert.Equal(t, expectedOrg.Type, org.Type)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestOrganizationRepository_GetByID_NotFound тестирует случай, когда организация не найдена
func TestOrganizationRepository_GetByID_NotFound(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewOrganizationRepository(db)
orgID := uuid.New()
// Ожидаем SQL запрос с пустым результатом
mock.ExpectQuery("SELECT (.+) FROM organizations").
WithArgs(orgID).
WillReturnError(sql.ErrNoRows)
// Act
org, err := repo.GetByID(context.Background(), orgID)
// Assert
assert.Error(t, err)
assert.Nil(t, org)
assert.Equal(t, "organization not found", err.Error())
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestUserRepository_Create тестирует создание пользователя
func TestUserRepository_Create(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewUserRepository(db)
userID := uuid.New()
orgID := uuid.New()
user := &models.User{
ID: userID,
OrganizationID: orgID,
Email: "test@example.com",
PasswordHash: "hashed_password",
Role: "admin",
CreatedAt: time.Now(),
}
password := "hashed_password"
// Ожидаем SQL запрос
mock.ExpectExec("INSERT INTO users").
WithArgs(userID, orgID, user.Email, password, user.Role, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
// Act
err = repo.Create(context.Background(), user, password)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestUserRepository_GetByEmail тестирует получение пользователя по email
func TestUserRepository_GetByEmail(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewUserRepository(db)
userID := uuid.New()
orgID := uuid.New()
email := "test@example.com"
expectedUser := &models.User{
ID: userID,
OrganizationID: orgID,
Email: email,
PasswordHash: "hashed_password",
Role: "admin",
}
// Ожидаем SQL запрос
rows := sqlmock.NewRows([]string{"id", "organization_id", "email", "password_hash", "role", "created_at"}).
AddRow(userID, orgID, email, "hashed_password", "admin", time.Now())
mock.ExpectQuery("SELECT (.+) FROM users").
WithArgs(email).
WillReturnRows(rows)
// Act
user, err := repo.GetByEmail(context.Background(), email)
// Assert
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, expectedUser.ID, user.ID)
assert.Equal(t, expectedUser.Email, user.Email)
assert.Equal(t, expectedUser.Role, user.Role)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestLocationRepository_Create тестирует создание места хранения
func TestLocationRepository_Create(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewLocationRepository(db)
locationID := uuid.New()
orgID := uuid.New()
location := &models.StorageLocation{
ID: locationID,
OrganizationID: orgID,
Name: "Test Location",
Address: "Test Address",
Type: "warehouse",
Coordinates: models.JSON{
"lat": 55.7558,
"lng": 37.6176,
},
CreatedAt: time.Now(),
}
// Ожидаем SQL запрос
mock.ExpectExec("INSERT INTO storage_locations").
WithArgs(locationID, orgID, sqlmock.AnyArg(), location.Name, location.Address, location.Type, sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
// Act
err = repo.Create(context.Background(), location)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestLocationRepository_GetByID тестирует получение места хранения по ID
func TestLocationRepository_GetByID(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewLocationRepository(db)
locationID := uuid.New()
orgID := uuid.New()
expectedLocation := &models.StorageLocation{
ID: locationID,
OrganizationID: orgID,
Name: "Test Location",
Address: "Test Address",
Type: "warehouse",
Coordinates: models.JSON{
"lat": 55.7558,
"lng": 37.6176,
},
}
// Ожидаем SQL запрос
rows := sqlmock.NewRows([]string{"id", "organization_id", "parent_id", "name", "address", "type", "coordinates", "created_at"}).
AddRow(locationID, orgID, nil, expectedLocation.Name, expectedLocation.Address, expectedLocation.Type, `{"lat":55.7558,"lng":37.6176}`, time.Now())
mock.ExpectQuery("SELECT (.+) FROM storage_locations").
WithArgs(locationID, orgID).
WillReturnRows(rows)
// Act
location, err := repo.GetByID(context.Background(), locationID, orgID)
// Assert
assert.NoError(t, err)
assert.NotNil(t, location)
assert.Equal(t, expectedLocation.ID, location.ID)
assert.Equal(t, expectedLocation.Name, location.Name)
assert.Equal(t, expectedLocation.Type, location.Type)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestItemRepository_Create тестирует создание товара
func TestItemRepository_Create(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewItemRepository(db)
itemID := uuid.New()
orgID := uuid.New()
item := &models.Item{
ID: itemID,
OrganizationID: orgID,
Name: "Test Item",
Description: "Test Description",
Category: "Test Category",
CreatedAt: time.Now(),
}
// Ожидаем SQL запрос
mock.ExpectExec("INSERT INTO items").
WithArgs(itemID, orgID, item.Name, item.Description, item.Category, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
// Act
err = repo.Create(context.Background(), item)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestOperationsRepository_PlaceItem тестирует размещение товара
func TestOperationsRepository_PlaceItem(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewOperationsRepository(db)
placementID := uuid.New()
orgID := uuid.New()
itemID := uuid.New()
locationID := uuid.New()
placement := &models.ItemPlacement{
ID: placementID,
OrganizationID: orgID,
ItemID: itemID,
LocationID: locationID,
Quantity: 5,
CreatedAt: time.Now(),
}
// Ожидаем SQL запрос
mock.ExpectExec("INSERT INTO item_placements").
WithArgs(placementID, orgID, itemID, locationID, placement.Quantity, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
// Act
err = repo.PlaceItem(context.Background(), placement)
// Assert
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
// TestOperationsRepository_Search тестирует поиск товаров с местами размещения
func TestOperationsRepository_Search(t *testing.T) {
// Arrange
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
repo := repository.NewOperationsRepository(db)
orgID := uuid.New()
itemID := uuid.New()
locationID := uuid.New()
// Ожидаем SQL запрос с JOIN
rows := sqlmock.NewRows([]string{
"i.id", "i.organization_id", "i.name", "i.description", "i.category", "i.created_at",
"sl.id", "sl.organization_id", "sl.parent_id", "sl.name", "sl.address", "sl.type", "sl.coordinates", "sl.created_at",
"ip.quantity",
}).AddRow(
itemID, orgID, "Test Item", "Test Description", "Test Category", time.Now(),
locationID, orgID, nil, "Test Location", "Test Address", "warehouse", `{"lat":55.7558,"lng":37.6176}`, time.Now(),
5,
)
mock.ExpectQuery("SELECT (.+) FROM items i").
WithArgs(orgID, "%test%").
WillReturnRows(rows)
// Act
results, err := repo.Search(context.Background(), orgID, "test", "", "")
// Assert
assert.NoError(t, err)
assert.NotNil(t, results)
assert.Len(t, results, 1)
assert.NoError(t, mock.ExpectationsWereMet())
}

View File

@@ -14,10 +14,12 @@ require (
)
require (
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -30,6 +32,9 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect

View File

@@ -1,3 +1,5 @@
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@@ -36,6 +38,7 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -60,6 +63,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -69,6 +73,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=

View File

@@ -0,0 +1,172 @@
package handlers
import (
"net/http"
"erp-mvp/core-service/internal/api/middleware"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/service"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type ItemHandler struct {
itemService service.ItemService
validate *validator.Validate
}
func NewItemHandler(itemService service.ItemService) *ItemHandler {
return &ItemHandler{
itemService: itemService,
validate: validator.New(),
}
}
// GetItems получает все товары организации
func (h *ItemHandler) GetItems(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
items, err := h.itemService.GetItems(c.Request.Context(), claims.OrganizationID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get items"})
return
}
c.JSON(http.StatusOK, items)
}
// CreateItem создает новый товар
func (h *ItemHandler) CreateItem(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var req models.CreateItemRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
item, err := h.itemService.CreateItem(c.Request.Context(), claims.OrganizationID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create item"})
return
}
c.JSON(http.StatusCreated, item)
}
// GetItem получает товар по ID
func (h *ItemHandler) GetItem(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"})
return
}
item, err := h.itemService.GetItem(c.Request.Context(), id, claims.OrganizationID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"})
return
}
c.JSON(http.StatusOK, item)
}
// UpdateItem обновляет товар
func (h *ItemHandler) UpdateItem(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"})
return
}
var req models.CreateItemRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
item, err := h.itemService.UpdateItem(c.Request.Context(), id, claims.OrganizationID, &req)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"})
return
}
c.JSON(http.StatusOK, item)
}
// DeleteItem удаляет товар
func (h *ItemHandler) DeleteItem(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"})
return
}
if err := h.itemService.DeleteItem(c.Request.Context(), id, claims.OrganizationID); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// SearchItems ищет товары
func (h *ItemHandler) SearchItems(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
query := c.Query("q")
category := c.Query("category")
items, err := h.itemService.SearchItems(c.Request.Context(), claims.OrganizationID, query, category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search items"})
return
}
c.JSON(http.StatusOK, items)
}

View File

@@ -0,0 +1,176 @@
package handlers
import (
"net/http"
"erp-mvp/core-service/internal/api/middleware"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/service"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type LocationHandler struct {
locationService service.LocationService
validate *validator.Validate
}
func NewLocationHandler(locationService service.LocationService) *LocationHandler {
return &LocationHandler{
locationService: locationService,
validate: validator.New(),
}
}
// GetLocations получает все места хранения организации
func (h *LocationHandler) GetLocations(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
locations, err := h.locationService.GetLocations(c.Request.Context(), claims.OrganizationID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get locations"})
return
}
c.JSON(http.StatusOK, locations)
}
// CreateLocation создает новое место хранения
func (h *LocationHandler) CreateLocation(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var req models.CreateLocationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
location, err := h.locationService.CreateLocation(c.Request.Context(), claims.OrganizationID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create location"})
return
}
c.JSON(http.StatusCreated, location)
}
// GetLocation получает место хранения по ID
func (h *LocationHandler) GetLocation(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid location ID"})
return
}
location, err := h.locationService.GetLocation(c.Request.Context(), id, claims.OrganizationID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
return
}
c.JSON(http.StatusOK, location)
}
// UpdateLocation обновляет место хранения
func (h *LocationHandler) UpdateLocation(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid location ID"})
return
}
var req models.CreateLocationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
location, err := h.locationService.UpdateLocation(c.Request.Context(), id, claims.OrganizationID, &req)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
return
}
c.JSON(http.StatusOK, location)
}
// DeleteLocation удаляет место хранения
func (h *LocationHandler) DeleteLocation(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid location ID"})
return
}
if err := h.locationService.DeleteLocation(c.Request.Context(), id, claims.OrganizationID); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// GetChildren получает дочерние места хранения
func (h *LocationHandler) GetChildren(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
parentIDStr := c.Param("id")
parentID, err := uuid.Parse(parentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid parent location ID"})
return
}
children, err := h.locationService.GetChildren(c.Request.Context(), parentID, claims.OrganizationID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Parent location not found"})
return
}
c.JSON(http.StatusOK, children)
}

View File

@@ -0,0 +1,227 @@
package handlers
import (
"net/http"
"erp-mvp/core-service/internal/api/middleware"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/service"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type OperationsHandler struct {
operationsService service.OperationsService
validate *validator.Validate
}
func NewOperationsHandler(operationsService service.OperationsService) *OperationsHandler {
return &OperationsHandler{
operationsService: operationsService,
validate: validator.New(),
}
}
// PlaceItem размещает товар в месте хранения
func (h *OperationsHandler) PlaceItem(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var req models.PlaceItemRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
placement, err := h.operationsService.PlaceItem(c.Request.Context(), claims.OrganizationID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to place item"})
return
}
c.JSON(http.StatusCreated, placement)
}
// MoveItem перемещает товар в другое место хранения
func (h *OperationsHandler) MoveItem(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
placementIDStr := c.Param("id")
placementID, err := uuid.Parse(placementIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid placement ID"})
return
}
var req struct {
NewLocationID uuid.UUID `json:"new_location_id" validate:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
if err := h.operationsService.MoveItem(c.Request.Context(), placementID, req.NewLocationID, claims.OrganizationID); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Placement not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Item moved successfully"})
}
// GetItemPlacements получает все размещения товара
func (h *OperationsHandler) GetItemPlacements(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
itemIDStr := c.Param("item_id")
itemID, err := uuid.Parse(itemIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"})
return
}
placements, err := h.operationsService.GetItemPlacements(c.Request.Context(), itemID, claims.OrganizationID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Item not found"})
return
}
c.JSON(http.StatusOK, placements)
}
// GetLocationPlacements получает все товары в месте хранения
func (h *OperationsHandler) GetLocationPlacements(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
locationIDStr := c.Param("location_id")
locationID, err := uuid.Parse(locationIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid location ID"})
return
}
placements, err := h.operationsService.GetLocationPlacements(c.Request.Context(), locationID, claims.OrganizationID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Location not found"})
return
}
c.JSON(http.StatusOK, placements)
}
// UpdateQuantity обновляет количество товара в размещении
func (h *OperationsHandler) UpdateQuantity(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
placementIDStr := c.Param("id")
placementID, err := uuid.Parse(placementIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid placement ID"})
return
}
var req struct {
Quantity int `json:"quantity" validate:"required,min=1"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()})
return
}
if err := h.operationsService.UpdateQuantity(c.Request.Context(), placementID, req.Quantity, claims.OrganizationID); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Placement not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Quantity updated successfully"})
}
// DeletePlacement удаляет размещение товара
func (h *OperationsHandler) DeletePlacement(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
placementIDStr := c.Param("id")
placementID, err := uuid.Parse(placementIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid placement ID"})
return
}
if err := h.operationsService.DeletePlacement(c.Request.Context(), placementID, claims.OrganizationID); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Placement not found"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// Search выполняет поиск товаров с местами размещения
func (h *OperationsHandler) Search(c *gin.Context) {
claims := middleware.GetClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var req models.SearchRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters"})
return
}
// Устанавливаем значения по умолчанию
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
response, err := h.operationsService.Search(c.Request.Context(), claims.OrganizationID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search"})
return
}
c.JSON(http.StatusOK, response)
}

View File

@@ -5,7 +5,9 @@ import (
"strings"
"erp-mvp/core-service/internal/auth"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AuthMiddleware struct {
@@ -55,3 +57,33 @@ func (m *AuthMiddleware) AuthRequired() gin.HandlerFunc {
c.Next()
}
}
// GetClaims получает claims из контекста Gin
func GetClaims(c *gin.Context) *auth.Claims {
userID, exists := c.Get("user_id")
if !exists {
return nil
}
orgID, exists := c.Get("organization_id")
if !exists {
return nil
}
email, exists := c.Get("email")
if !exists {
return nil
}
role, exists := c.Get("role")
if !exists {
return nil
}
return &auth.Claims{
UserID: userID.(uuid.UUID),
OrganizationID: orgID.(uuid.UUID),
Email: email.(string),
Role: role.(string),
}
}

View File

@@ -5,13 +5,13 @@ import (
"database/sql"
"net/http"
"erp-mvp/core-service/internal/api/handlers"
"erp-mvp/core-service/internal/api/middleware"
"erp-mvp/core-service/internal/auth"
"erp-mvp/core-service/internal/config"
"erp-mvp/core-service/internal/logger"
"erp-mvp/core-service/internal/repository"
"erp-mvp/core-service/internal/service"
"erp-mvp/core-service/internal/api/handlers"
"erp-mvp/core-service/internal/api/middleware"
"github.com/gin-gonic/gin"
)
@@ -23,10 +23,16 @@ type Server struct {
router *gin.Engine
// Services
authService service.AuthService
authService service.AuthService
locationService service.LocationService
itemService service.ItemService
operationsService service.OperationsService
// Handlers
authHandler *handlers.AuthHandler
authHandler *handlers.AuthHandler
locationHandler *handlers.LocationHandler
itemHandler *handlers.ItemHandler
operationsHandler *handlers.OperationsHandler
// Middleware
authMiddleware *middleware.AuthMiddleware
@@ -39,24 +45,39 @@ func NewServer(cfg *config.Config, db *sql.DB, log logger.Logger) *Server {
// Инициализируем репозитории
orgRepo := repository.NewOrganizationRepository(db)
userRepo := repository.NewUserRepository(db)
locationRepo := repository.NewLocationRepository(db)
itemRepo := repository.NewItemRepository(db)
operationsRepo := repository.NewOperationsRepository(db)
// Инициализируем сервисы
authService := service.NewAuthService(orgRepo, userRepo, jwtService)
locationService := service.NewLocationService(locationRepo)
itemService := service.NewItemService(itemRepo)
operationsService := service.NewOperationsService(operationsRepo, itemRepo, locationRepo)
// Инициализируем handlers
authHandler := handlers.NewAuthHandler(authService)
locationHandler := handlers.NewLocationHandler(locationService)
itemHandler := handlers.NewItemHandler(itemService)
operationsHandler := handlers.NewOperationsHandler(operationsService)
// Инициализируем middleware
authMiddleware := middleware.NewAuthMiddleware(jwtService)
server := &Server{
config: cfg,
db: db,
logger: log,
router: gin.Default(),
authService: authService,
authHandler: authHandler,
authMiddleware: authMiddleware,
config: cfg,
db: db,
logger: log,
router: gin.Default(),
authService: authService,
locationService: locationService,
itemService: itemService,
operationsService: operationsService,
authHandler: authHandler,
locationHandler: locationHandler,
itemHandler: itemHandler,
operationsHandler: operationsHandler,
authMiddleware: authMiddleware,
}
server.setupRoutes()
@@ -86,23 +107,29 @@ func (s *Server) setupRoutes() {
protected.PUT("/organizations/:id", s.updateOrganization)
// Locations
protected.GET("/locations", s.getLocations)
protected.POST("/locations", s.createLocation)
protected.GET("/locations/:id", s.getLocation)
protected.PUT("/locations/:id", s.updateLocation)
protected.DELETE("/locations/:id", s.deleteLocation)
protected.GET("/locations", s.locationHandler.GetLocations)
protected.POST("/locations", s.locationHandler.CreateLocation)
protected.GET("/locations/:id", s.locationHandler.GetLocation)
protected.PUT("/locations/:id", s.locationHandler.UpdateLocation)
protected.DELETE("/locations/:id", s.locationHandler.DeleteLocation)
protected.GET("/locations/:id/children", s.locationHandler.GetChildren)
// Items
protected.GET("/items", s.getItems)
protected.POST("/items", s.createItem)
protected.GET("/items/:id", s.getItem)
protected.PUT("/items/:id", s.updateItem)
protected.DELETE("/items/:id", s.deleteItem)
protected.GET("/items", s.itemHandler.GetItems)
protected.POST("/items", s.itemHandler.CreateItem)
protected.GET("/items/:id", s.itemHandler.GetItem)
protected.PUT("/items/:id", s.itemHandler.UpdateItem)
protected.DELETE("/items/:id", s.itemHandler.DeleteItem)
protected.GET("/items/search", s.itemHandler.SearchItems)
// Operations
protected.POST("/operations/place-item", s.placeItem)
protected.POST("/operations/move-item", s.moveItem)
protected.GET("/operations/search", s.search)
protected.POST("/operations/place-item", s.operationsHandler.PlaceItem)
protected.POST("/operations/move-item/:id", s.operationsHandler.MoveItem)
protected.GET("/operations/search", s.operationsHandler.Search)
protected.GET("/operations/items/:item_id/placements", s.operationsHandler.GetItemPlacements)
protected.GET("/operations/locations/:location_id/placements", s.operationsHandler.GetLocationPlacements)
protected.PUT("/operations/placements/:id/quantity", s.operationsHandler.UpdateQuantity)
protected.DELETE("/operations/placements/:id", s.operationsHandler.DeletePlacement)
// Templates
protected.GET("/templates", s.getTemplates)
@@ -113,7 +140,7 @@ func (s *Server) setupRoutes() {
func (s *Server) healthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"status": "ok",
"service": "erp-mvp-core",
})
}
@@ -127,58 +154,6 @@ func (s *Server) updateOrganization(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) getLocations(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) createLocation(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) getLocation(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) updateLocation(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) deleteLocation(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) getItems(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) createItem(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) getItem(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) updateItem(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) deleteItem(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) placeItem(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) moveItem(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) search(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
func (s *Server) getTemplates(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}

View File

@@ -129,8 +129,8 @@ type SearchRequest struct {
// SearchResponse результат поиска
type SearchResponse struct {
Items []ItemWithLocation `json:"items"`
TotalCount int `json:"total_count"`
Items []*ItemWithLocation `json:"items"`
TotalCount int `json:"total_count"`
}
// ItemWithLocation товар с информацией о месте размещения

View File

@@ -0,0 +1,224 @@
package repository
import (
"context"
"database/sql"
"fmt"
"erp-mvp/core-service/internal/models"
"github.com/google/uuid"
)
type ItemRepository interface {
Create(ctx context.Context, item *models.Item) error
GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error)
GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error)
Update(ctx context.Context, item *models.Item) error
Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
Search(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error)
}
type itemRepository struct {
db *sql.DB
}
func NewItemRepository(db *sql.DB) ItemRepository {
return &itemRepository{db: db}
}
func (r *itemRepository) Create(ctx context.Context, item *models.Item) error {
query := `
INSERT INTO items (id, organization_id, name, description, category, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err := r.db.ExecContext(ctx, query,
item.ID,
item.OrganizationID,
item.Name,
item.Description,
item.Category,
item.CreatedAt,
)
if err != nil {
return fmt.Errorf("failed to create item: %w", err)
}
return nil
}
func (r *itemRepository) GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error) {
query := `
SELECT id, organization_id, name, description, category, created_at
FROM items
WHERE id = $1 AND organization_id = $2
`
item := &models.Item{}
err := r.db.QueryRowContext(ctx, query, id, orgID).Scan(
&item.ID,
&item.OrganizationID,
&item.Name,
&item.Description,
&item.Category,
&item.CreatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("item not found")
}
return nil, fmt.Errorf("failed to get item: %w", err)
}
return item, nil
}
func (r *itemRepository) GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error) {
query := `
SELECT id, organization_id, name, description, category, created_at
FROM items
WHERE organization_id = $1
ORDER BY name
`
rows, err := r.db.QueryContext(ctx, query, orgID)
if err != nil {
return nil, fmt.Errorf("failed to query items: %w", err)
}
defer rows.Close()
var items []*models.Item
for rows.Next() {
item := &models.Item{}
err := rows.Scan(
&item.ID,
&item.OrganizationID,
&item.Name,
&item.Description,
&item.Category,
&item.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan item: %w", err)
}
items = append(items, item)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating items: %w", err)
}
return items, nil
}
func (r *itemRepository) Update(ctx context.Context, item *models.Item) error {
query := `
UPDATE items
SET name = $3, description = $4, category = $5
WHERE id = $1 AND organization_id = $2
`
result, err := r.db.ExecContext(ctx, query,
item.ID,
item.OrganizationID,
item.Name,
item.Description,
item.Category,
)
if err != nil {
return fmt.Errorf("failed to update item: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("item not found")
}
return nil
}
func (r *itemRepository) Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error {
query := `
DELETE FROM items
WHERE id = $1 AND organization_id = $2
`
result, err := r.db.ExecContext(ctx, query, id, orgID)
if err != nil {
return fmt.Errorf("failed to delete item: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("item not found")
}
return nil
}
func (r *itemRepository) Search(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error) {
baseQuery := `
SELECT id, organization_id, name, description, category, created_at
FROM items
WHERE organization_id = $1
`
var args []interface{}
args = append(args, orgID)
argIndex := 2
if query != "" {
baseQuery += fmt.Sprintf(" AND (name ILIKE $%d OR description ILIKE $%d)", argIndex, argIndex)
args = append(args, "%"+query+"%")
argIndex++
}
if category != "" {
baseQuery += fmt.Sprintf(" AND category = $%d", argIndex)
args = append(args, category)
argIndex++
}
baseQuery += " ORDER BY name"
rows, err := r.db.QueryContext(ctx, baseQuery, args...)
if err != nil {
return nil, fmt.Errorf("failed to search items: %w", err)
}
defer rows.Close()
var items []*models.Item
for rows.Next() {
item := &models.Item{}
err := rows.Scan(
&item.ID,
&item.OrganizationID,
&item.Name,
&item.Description,
&item.Category,
&item.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan item: %w", err)
}
items = append(items, item)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating search results: %w", err)
}
return items, nil
}

View File

@@ -0,0 +1,271 @@
package repository
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"erp-mvp/core-service/internal/models"
"github.com/google/uuid"
)
type LocationRepository interface {
Create(ctx context.Context, location *models.StorageLocation) error
GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.StorageLocation, error)
GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error)
Update(ctx context.Context, location *models.StorageLocation) error
Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
GetChildren(ctx context.Context, parentID uuid.UUID, orgID uuid.UUID) ([]*models.StorageLocation, error)
}
type locationRepository struct {
db *sql.DB
}
func NewLocationRepository(db *sql.DB) LocationRepository {
return &locationRepository{db: db}
}
func (r *locationRepository) Create(ctx context.Context, location *models.StorageLocation) error {
query := `
INSERT INTO storage_locations (id, organization_id, parent_id, name, address, type, coordinates, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
// Конвертируем JSON в строку
var coordinatesJSON string
if location.Coordinates != nil {
coords, err := json.Marshal(location.Coordinates)
if err != nil {
return fmt.Errorf("failed to marshal coordinates: %w", err)
}
coordinatesJSON = string(coords)
}
_, err := r.db.ExecContext(ctx, query,
location.ID,
location.OrganizationID,
location.ParentID,
location.Name,
location.Address,
location.Type,
coordinatesJSON,
location.CreatedAt,
)
if err != nil {
return fmt.Errorf("failed to create storage location: %w", err)
}
return nil
}
func (r *locationRepository) GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.StorageLocation, error) {
query := `
SELECT id, organization_id, parent_id, name, address, type, coordinates, created_at
FROM storage_locations
WHERE id = $1 AND organization_id = $2
`
var coordinatesJSON []byte
location := &models.StorageLocation{}
err := r.db.QueryRowContext(ctx, query, id, orgID).Scan(
&location.ID,
&location.OrganizationID,
&location.ParentID,
&location.Name,
&location.Address,
&location.Type,
&coordinatesJSON,
&location.CreatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("storage location not found")
}
return nil, fmt.Errorf("failed to get storage location: %w", err)
}
// Конвертируем JSON строку в map
if len(coordinatesJSON) > 0 {
err = json.Unmarshal(coordinatesJSON, &location.Coordinates)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal coordinates: %w", err)
}
} else {
location.Coordinates = make(models.JSON)
}
return location, nil
}
func (r *locationRepository) GetByOrganization(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error) {
query := `
SELECT id, organization_id, parent_id, name, address, type, coordinates, created_at
FROM storage_locations
WHERE organization_id = $1
ORDER BY name
`
rows, err := r.db.QueryContext(ctx, query, orgID)
if err != nil {
return nil, fmt.Errorf("failed to query storage locations: %w", err)
}
defer rows.Close()
var locations []*models.StorageLocation
for rows.Next() {
var coordinatesJSON []byte
location := &models.StorageLocation{}
err := rows.Scan(
&location.ID,
&location.OrganizationID,
&location.ParentID,
&location.Name,
&location.Address,
&location.Type,
&coordinatesJSON,
&location.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan storage location: %w", err)
}
// Конвертируем JSON строку в map
if len(coordinatesJSON) > 0 {
err = json.Unmarshal(coordinatesJSON, &location.Coordinates)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal coordinates: %w", err)
}
} else {
location.Coordinates = make(models.JSON)
}
locations = append(locations, location)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating storage locations: %w", err)
}
return locations, nil
}
func (r *locationRepository) Update(ctx context.Context, location *models.StorageLocation) error {
query := `
UPDATE storage_locations
SET parent_id = $3, name = $4, address = $5, type = $6, coordinates = $7
WHERE id = $1 AND organization_id = $2
`
// Конвертируем JSON в строку
var coordinatesJSON string
if location.Coordinates != nil {
coords, err := json.Marshal(location.Coordinates)
if err != nil {
return fmt.Errorf("failed to marshal coordinates: %w", err)
}
coordinatesJSON = string(coords)
}
result, err := r.db.ExecContext(ctx, query,
location.ID,
location.OrganizationID,
location.ParentID,
location.Name,
location.Address,
location.Type,
coordinatesJSON,
)
if err != nil {
return fmt.Errorf("failed to update storage location: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("storage location not found")
}
return nil
}
func (r *locationRepository) Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error {
query := `
DELETE FROM storage_locations
WHERE id = $1 AND organization_id = $2
`
result, err := r.db.ExecContext(ctx, query, id, orgID)
if err != nil {
return fmt.Errorf("failed to delete storage location: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("storage location not found")
}
return nil
}
func (r *locationRepository) GetChildren(ctx context.Context, parentID uuid.UUID, orgID uuid.UUID) ([]*models.StorageLocation, error) {
query := `
SELECT id, organization_id, parent_id, name, address, type, coordinates, created_at
FROM storage_locations
WHERE parent_id = $1 AND organization_id = $2
ORDER BY name
`
rows, err := r.db.QueryContext(ctx, query, parentID, orgID)
if err != nil {
return nil, fmt.Errorf("failed to query child locations: %w", err)
}
defer rows.Close()
var locations []*models.StorageLocation
for rows.Next() {
var coordinatesJSON []byte
location := &models.StorageLocation{}
err := rows.Scan(
&location.ID,
&location.OrganizationID,
&location.ParentID,
&location.Name,
&location.Address,
&location.Type,
&coordinatesJSON,
&location.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan child location: %w", err)
}
// Конвертируем JSON строку в map
if len(coordinatesJSON) > 0 {
err = json.Unmarshal(coordinatesJSON, &location.Coordinates)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal coordinates: %w", err)
}
} else {
location.Coordinates = make(models.JSON)
}
locations = append(locations, location)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating child locations: %w", err)
}
return locations, nil
}

View File

@@ -0,0 +1,316 @@
package repository
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"erp-mvp/core-service/internal/models"
"github.com/google/uuid"
)
type OperationsRepository interface {
PlaceItem(ctx context.Context, placement *models.ItemPlacement) error
MoveItem(ctx context.Context, placementID uuid.UUID, newLocationID uuid.UUID, orgID uuid.UUID) error
GetByItem(ctx context.Context, itemID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error)
GetByLocation(ctx context.Context, locationID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error)
GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.ItemPlacement, error)
UpdateQuantity(ctx context.Context, id uuid.UUID, quantity int, orgID uuid.UUID) error
Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
Search(ctx context.Context, orgID uuid.UUID, query string, category string, address string) ([]*models.ItemWithLocation, error)
}
type operationsRepository struct {
db *sql.DB
}
func NewOperationsRepository(db *sql.DB) OperationsRepository {
return &operationsRepository{db: db}
}
func (r *operationsRepository) PlaceItem(ctx context.Context, placement *models.ItemPlacement) error {
query := `
INSERT INTO item_placements (id, organization_id, item_id, location_id, quantity, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err := r.db.ExecContext(ctx, query,
placement.ID,
placement.OrganizationID,
placement.ItemID,
placement.LocationID,
placement.Quantity,
placement.CreatedAt,
)
if err != nil {
return fmt.Errorf("failed to place item: %w", err)
}
return nil
}
func (r *operationsRepository) MoveItem(ctx context.Context, placementID uuid.UUID, newLocationID uuid.UUID, orgID uuid.UUID) error {
query := `
UPDATE item_placements
SET location_id = $2
WHERE id = $1 AND organization_id = $3
`
result, err := r.db.ExecContext(ctx, query, placementID, newLocationID, orgID)
if err != nil {
return fmt.Errorf("failed to move item: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("item placement not found")
}
return nil
}
func (r *operationsRepository) GetByItem(ctx context.Context, itemID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error) {
query := `
SELECT id, organization_id, item_id, location_id, quantity, created_at
FROM item_placements
WHERE item_id = $1 AND organization_id = $2
ORDER BY created_at DESC
`
rows, err := r.db.QueryContext(ctx, query, itemID, orgID)
if err != nil {
return nil, fmt.Errorf("failed to query item placements: %w", err)
}
defer rows.Close()
var placements []*models.ItemPlacement
for rows.Next() {
placement := &models.ItemPlacement{}
err := rows.Scan(
&placement.ID,
&placement.OrganizationID,
&placement.ItemID,
&placement.LocationID,
&placement.Quantity,
&placement.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan item placement: %w", err)
}
placements = append(placements, placement)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating item placements: %w", err)
}
return placements, nil
}
func (r *operationsRepository) GetByLocation(ctx context.Context, locationID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error) {
query := `
SELECT id, organization_id, item_id, location_id, quantity, created_at
FROM item_placements
WHERE location_id = $1 AND organization_id = $2
ORDER BY created_at DESC
`
rows, err := r.db.QueryContext(ctx, query, locationID, orgID)
if err != nil {
return nil, fmt.Errorf("failed to query location placements: %w", err)
}
defer rows.Close()
var placements []*models.ItemPlacement
for rows.Next() {
placement := &models.ItemPlacement{}
err := rows.Scan(
&placement.ID,
&placement.OrganizationID,
&placement.ItemID,
&placement.LocationID,
&placement.Quantity,
&placement.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan item placement: %w", err)
}
placements = append(placements, placement)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating location placements: %w", err)
}
return placements, nil
}
func (r *operationsRepository) GetByID(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.ItemPlacement, error) {
query := `
SELECT id, organization_id, item_id, location_id, quantity, created_at
FROM item_placements
WHERE id = $1 AND organization_id = $2
`
placement := &models.ItemPlacement{}
err := r.db.QueryRowContext(ctx, query, id, orgID).Scan(
&placement.ID,
&placement.OrganizationID,
&placement.ItemID,
&placement.LocationID,
&placement.Quantity,
&placement.CreatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("item placement not found")
}
return nil, fmt.Errorf("failed to get item placement: %w", err)
}
return placement, nil
}
func (r *operationsRepository) UpdateQuantity(ctx context.Context, id uuid.UUID, quantity int, orgID uuid.UUID) error {
query := `
UPDATE item_placements
SET quantity = $2
WHERE id = $1 AND organization_id = $3
`
result, err := r.db.ExecContext(ctx, query, id, quantity, orgID)
if err != nil {
return fmt.Errorf("failed to update quantity: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("item placement not found")
}
return nil
}
func (r *operationsRepository) Delete(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error {
query := `
DELETE FROM item_placements
WHERE id = $1 AND organization_id = $2
`
result, err := r.db.ExecContext(ctx, query, id, orgID)
if err != nil {
return fmt.Errorf("failed to delete item placement: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("item placement not found")
}
return nil
}
func (r *operationsRepository) Search(ctx context.Context, orgID uuid.UUID, query string, category string, address string) ([]*models.ItemWithLocation, error) {
baseQuery := `
SELECT
i.id, i.organization_id, i.name, i.description, i.category, i.created_at,
sl.id, sl.organization_id, sl.parent_id, sl.name, sl.address, sl.type, sl.coordinates, sl.created_at,
ip.quantity
FROM items i
JOIN item_placements ip ON i.id = ip.item_id
JOIN storage_locations sl ON ip.location_id = sl.id
WHERE i.organization_id = $1 AND sl.organization_id = $1
`
var args []interface{}
args = append(args, orgID)
argIndex := 2
if query != "" {
baseQuery += fmt.Sprintf(" AND (i.name ILIKE $%d OR i.description ILIKE $%d)", argIndex, argIndex)
args = append(args, "%"+query+"%")
argIndex++
}
if category != "" {
baseQuery += fmt.Sprintf(" AND i.category = $%d", argIndex)
args = append(args, category)
argIndex++
}
if address != "" {
baseQuery += fmt.Sprintf(" AND sl.address ILIKE $%d", argIndex)
args = append(args, "%"+address+"%")
argIndex++
}
baseQuery += " ORDER BY i.name, sl.name"
rows, err := r.db.QueryContext(ctx, baseQuery, args...)
if err != nil {
return nil, fmt.Errorf("failed to search items with locations: %w", err)
}
defer rows.Close()
var results []*models.ItemWithLocation
for rows.Next() {
var coordinatesJSON []byte
itemWithLocation := &models.ItemWithLocation{}
err := rows.Scan(
&itemWithLocation.Item.ID,
&itemWithLocation.Item.OrganizationID,
&itemWithLocation.Item.Name,
&itemWithLocation.Item.Description,
&itemWithLocation.Item.Category,
&itemWithLocation.Item.CreatedAt,
&itemWithLocation.Location.ID,
&itemWithLocation.Location.OrganizationID,
&itemWithLocation.Location.ParentID,
&itemWithLocation.Location.Name,
&itemWithLocation.Location.Address,
&itemWithLocation.Location.Type,
&coordinatesJSON,
&itemWithLocation.Location.CreatedAt,
&itemWithLocation.Quantity,
)
if err != nil {
return nil, fmt.Errorf("failed to scan item with location: %w", err)
}
// Конвертируем JSON строку в map
if len(coordinatesJSON) > 0 {
err = json.Unmarshal(coordinatesJSON, &itemWithLocation.Location.Coordinates)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal coordinates: %w", err)
}
} else {
itemWithLocation.Location.Coordinates = make(models.JSON)
}
results = append(results, itemWithLocation)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating search results: %w", err)
}
return results, nil
}

View File

@@ -0,0 +1,126 @@
package service
import (
"context"
"time"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/repository"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type ItemService interface {
CreateItem(ctx context.Context, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error)
GetItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error)
GetItems(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error)
UpdateItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error)
DeleteItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
SearchItems(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error)
}
type itemService struct {
itemRepo repository.ItemRepository
logger *logrus.Logger
}
func NewItemService(itemRepo repository.ItemRepository) ItemService {
return &itemService{
itemRepo: itemRepo,
logger: logrus.New(),
}
}
func (s *itemService) CreateItem(ctx context.Context, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error) {
s.logger.Info("Creating item for organization: ", orgID)
item := &models.Item{
ID: uuid.New(),
OrganizationID: orgID,
Name: req.Name,
Description: req.Description,
Category: req.Category,
CreatedAt: time.Now(),
}
if err := s.itemRepo.Create(ctx, item); err != nil {
s.logger.Error("Failed to create item: ", err)
return nil, err
}
s.logger.Info("Item created successfully: ", item.ID)
return item, nil
}
func (s *itemService) GetItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.Item, error) {
s.logger.Info("Getting item: ", id, " for organization: ", orgID)
item, err := s.itemRepo.GetByID(ctx, id, orgID)
if err != nil {
s.logger.Error("Failed to get item: ", err)
return nil, err
}
return item, nil
}
func (s *itemService) GetItems(ctx context.Context, orgID uuid.UUID) ([]*models.Item, error) {
s.logger.Info("Getting all items for organization: ", orgID)
items, err := s.itemRepo.GetByOrganization(ctx, orgID)
if err != nil {
s.logger.Error("Failed to get items: ", err)
return nil, err
}
return items, nil
}
func (s *itemService) UpdateItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID, req *models.CreateItemRequest) (*models.Item, error) {
s.logger.Info("Updating item: ", id, " for organization: ", orgID)
// Сначала получаем существующий товар
item, err := s.itemRepo.GetByID(ctx, id, orgID)
if err != nil {
s.logger.Error("Failed to get item for update: ", err)
return nil, err
}
// Обновляем поля
item.Name = req.Name
item.Description = req.Description
item.Category = req.Category
if err := s.itemRepo.Update(ctx, item); err != nil {
s.logger.Error("Failed to update item: ", err)
return nil, err
}
s.logger.Info("Item updated successfully: ", item.ID)
return item, nil
}
func (s *itemService) DeleteItem(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error {
s.logger.Info("Deleting item: ", id, " for organization: ", orgID)
if err := s.itemRepo.Delete(ctx, id, orgID); err != nil {
s.logger.Error("Failed to delete item: ", err)
return err
}
s.logger.Info("Item deleted successfully: ", id)
return nil
}
func (s *itemService) SearchItems(ctx context.Context, orgID uuid.UUID, query string, category string) ([]*models.Item, error) {
s.logger.Info("Searching items for organization: ", orgID, " query: ", query, " category: ", category)
items, err := s.itemRepo.Search(ctx, orgID, query, category)
if err != nil {
s.logger.Error("Failed to search items: ", err)
return nil, err
}
return items, nil
}

View File

@@ -0,0 +1,130 @@
package service
import (
"context"
"time"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/repository"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type LocationService interface {
CreateLocation(ctx context.Context, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error)
GetLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.StorageLocation, error)
GetLocations(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error)
UpdateLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error)
DeleteLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error
GetChildren(ctx context.Context, parentID uuid.UUID, orgID uuid.UUID) ([]*models.StorageLocation, error)
}
type locationService struct {
locationRepo repository.LocationRepository
logger *logrus.Logger
}
func NewLocationService(locationRepo repository.LocationRepository) LocationService {
return &locationService{
locationRepo: locationRepo,
logger: logrus.New(),
}
}
func (s *locationService) CreateLocation(ctx context.Context, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
s.logger.Info("Creating location for organization: ", orgID)
location := &models.StorageLocation{
ID: uuid.New(),
OrganizationID: orgID,
ParentID: req.ParentID,
Name: req.Name,
Address: req.Address,
Type: req.Type,
Coordinates: req.Coordinates,
CreatedAt: time.Now(),
}
if err := s.locationRepo.Create(ctx, location); err != nil {
s.logger.Error("Failed to create location: ", err)
return nil, err
}
s.logger.Info("Location created successfully: ", location.ID)
return location, nil
}
func (s *locationService) GetLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID) (*models.StorageLocation, error) {
s.logger.Info("Getting location: ", id, " for organization: ", orgID)
location, err := s.locationRepo.GetByID(ctx, id, orgID)
if err != nil {
s.logger.Error("Failed to get location: ", err)
return nil, err
}
return location, nil
}
func (s *locationService) GetLocations(ctx context.Context, orgID uuid.UUID) ([]*models.StorageLocation, error) {
s.logger.Info("Getting all locations for organization: ", orgID)
locations, err := s.locationRepo.GetByOrganization(ctx, orgID)
if err != nil {
s.logger.Error("Failed to get locations: ", err)
return nil, err
}
return locations, nil
}
func (s *locationService) UpdateLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID, req *models.CreateLocationRequest) (*models.StorageLocation, error) {
s.logger.Info("Updating location: ", id, " for organization: ", orgID)
// Сначала получаем существующую локацию
location, err := s.locationRepo.GetByID(ctx, id, orgID)
if err != nil {
s.logger.Error("Failed to get location for update: ", err)
return nil, err
}
// Обновляем поля
location.ParentID = req.ParentID
location.Name = req.Name
location.Address = req.Address
location.Type = req.Type
location.Coordinates = req.Coordinates
if err := s.locationRepo.Update(ctx, location); err != nil {
s.logger.Error("Failed to update location: ", err)
return nil, err
}
s.logger.Info("Location updated successfully: ", location.ID)
return location, nil
}
func (s *locationService) DeleteLocation(ctx context.Context, id uuid.UUID, orgID uuid.UUID) error {
s.logger.Info("Deleting location: ", id, " for organization: ", orgID)
if err := s.locationRepo.Delete(ctx, id, orgID); err != nil {
s.logger.Error("Failed to delete location: ", err)
return err
}
s.logger.Info("Location deleted successfully: ", id)
return nil
}
func (s *locationService) GetChildren(ctx context.Context, parentID uuid.UUID, orgID uuid.UUID) ([]*models.StorageLocation, error) {
s.logger.Info("Getting children for location: ", parentID, " in organization: ", orgID)
children, err := s.locationRepo.GetChildren(ctx, parentID, orgID)
if err != nil {
s.logger.Error("Failed to get children: ", err)
return nil, err
}
return children, nil
}

View File

@@ -0,0 +1,192 @@
package service
import (
"context"
"time"
"erp-mvp/core-service/internal/models"
"erp-mvp/core-service/internal/repository"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type OperationsService interface {
PlaceItem(ctx context.Context, orgID uuid.UUID, req *models.PlaceItemRequest) (*models.ItemPlacement, error)
MoveItem(ctx context.Context, placementID uuid.UUID, newLocationID uuid.UUID, orgID uuid.UUID) error
GetItemPlacements(ctx context.Context, itemID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error)
GetLocationPlacements(ctx context.Context, locationID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error)
UpdateQuantity(ctx context.Context, placementID uuid.UUID, quantity int, orgID uuid.UUID) error
DeletePlacement(ctx context.Context, placementID uuid.UUID, orgID uuid.UUID) error
Search(ctx context.Context, orgID uuid.UUID, req *models.SearchRequest) (*models.SearchResponse, error)
}
type operationsService struct {
operationsRepo repository.OperationsRepository
itemRepo repository.ItemRepository
locationRepo repository.LocationRepository
logger *logrus.Logger
}
func NewOperationsService(operationsRepo repository.OperationsRepository, itemRepo repository.ItemRepository, locationRepo repository.LocationRepository) OperationsService {
return &operationsService{
operationsRepo: operationsRepo,
itemRepo: itemRepo,
locationRepo: locationRepo,
logger: logrus.New(),
}
}
func (s *operationsService) PlaceItem(ctx context.Context, orgID uuid.UUID, req *models.PlaceItemRequest) (*models.ItemPlacement, error) {
s.logger.Info("Placing item: ", req.ItemID, " in location: ", req.LocationID, " for organization: ", orgID)
// Проверяем, что товар существует и принадлежит организации
_, err := s.itemRepo.GetByID(ctx, req.ItemID, orgID)
if err != nil {
s.logger.Error("Item not found or not accessible: ", err)
return nil, err
}
// Проверяем, что место хранения существует и принадлежит организации
_, err = s.locationRepo.GetByID(ctx, req.LocationID, orgID)
if err != nil {
s.logger.Error("Location not found or not accessible: ", err)
return nil, err
}
placement := &models.ItemPlacement{
ID: uuid.New(),
OrganizationID: orgID,
ItemID: req.ItemID,
LocationID: req.LocationID,
Quantity: req.Quantity,
CreatedAt: time.Now(),
}
if err := s.operationsRepo.PlaceItem(ctx, placement); err != nil {
s.logger.Error("Failed to place item: ", err)
return nil, err
}
s.logger.Info("Item placed successfully: ", placement.ID)
return placement, nil
}
func (s *operationsService) MoveItem(ctx context.Context, placementID uuid.UUID, newLocationID uuid.UUID, orgID uuid.UUID) error {
s.logger.Info("Moving item placement: ", placementID, " to location: ", newLocationID, " for organization: ", orgID)
// Проверяем, что размещение существует и принадлежит организации
placement, err := s.operationsRepo.GetByID(ctx, placementID, orgID)
if err != nil {
s.logger.Error("Item placement not found or not accessible: ", err)
return err
}
// Проверяем, что новое место хранения существует и принадлежит организации
_, err = s.locationRepo.GetByID(ctx, newLocationID, orgID)
if err != nil {
s.logger.Error("New location not found or not accessible: ", err)
return err
}
if err := s.operationsRepo.MoveItem(ctx, placementID, newLocationID, orgID); err != nil {
s.logger.Error("Failed to move item: ", err)
return err
}
s.logger.Info("Item moved successfully from location: ", placement.LocationID, " to: ", newLocationID)
return nil
}
func (s *operationsService) GetItemPlacements(ctx context.Context, itemID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error) {
s.logger.Info("Getting placements for item: ", itemID, " in organization: ", orgID)
// Проверяем, что товар существует и принадлежит организации
_, err := s.itemRepo.GetByID(ctx, itemID, orgID)
if err != nil {
s.logger.Error("Item not found or not accessible: ", err)
return nil, err
}
placements, err := s.operationsRepo.GetByItem(ctx, itemID, orgID)
if err != nil {
s.logger.Error("Failed to get item placements: ", err)
return nil, err
}
return placements, nil
}
func (s *operationsService) GetLocationPlacements(ctx context.Context, locationID uuid.UUID, orgID uuid.UUID) ([]*models.ItemPlacement, error) {
s.logger.Info("Getting placements for location: ", locationID, " in organization: ", orgID)
// Проверяем, что место хранения существует и принадлежит организации
_, err := s.locationRepo.GetByID(ctx, locationID, orgID)
if err != nil {
s.logger.Error("Location not found or not accessible: ", err)
return nil, err
}
placements, err := s.operationsRepo.GetByLocation(ctx, locationID, orgID)
if err != nil {
s.logger.Error("Failed to get location placements: ", err)
return nil, err
}
return placements, nil
}
func (s *operationsService) UpdateQuantity(ctx context.Context, placementID uuid.UUID, quantity int, orgID uuid.UUID) error {
s.logger.Info("Updating quantity for placement: ", placementID, " to: ", quantity, " in organization: ", orgID)
// Проверяем, что размещение существует и принадлежит организации
_, err := s.operationsRepo.GetByID(ctx, placementID, orgID)
if err != nil {
s.logger.Error("Item placement not found or not accessible: ", err)
return err
}
if err := s.operationsRepo.UpdateQuantity(ctx, placementID, quantity, orgID); err != nil {
s.logger.Error("Failed to update quantity: ", err)
return err
}
s.logger.Info("Quantity updated successfully for placement: ", placementID)
return nil
}
func (s *operationsService) DeletePlacement(ctx context.Context, placementID uuid.UUID, orgID uuid.UUID) error {
s.logger.Info("Deleting placement: ", placementID, " for organization: ", orgID)
// Проверяем, что размещение существует и принадлежит организации
_, err := s.operationsRepo.GetByID(ctx, placementID, orgID)
if err != nil {
s.logger.Error("Item placement not found or not accessible: ", err)
return err
}
if err := s.operationsRepo.Delete(ctx, placementID, orgID); err != nil {
s.logger.Error("Failed to delete placement: ", err)
return err
}
s.logger.Info("Placement deleted successfully: ", placementID)
return nil
}
func (s *operationsService) Search(ctx context.Context, orgID uuid.UUID, req *models.SearchRequest) (*models.SearchResponse, error) {
s.logger.Info("Searching items with locations for organization: ", orgID, " query: ", req.Query)
results, err := s.operationsRepo.Search(ctx, orgID, req.Query, req.Category, req.Address)
if err != nil {
s.logger.Error("Failed to search items with locations: ", err)
return nil, err
}
response := &models.SearchResponse{
Items: results,
TotalCount: len(results),
}
return response, nil
}

BIN
core-service/main Executable file

Binary file not shown.