tercul-backend/internal/app/translation/commands.go
google-labs-jules[bot] b03820de02 Refactor: Improve quality, testing, and core business logic.
This commit introduces a significant refactoring to improve the application's quality, test coverage, and production readiness, focusing on core localization and business logic features.

Key changes include:
- Consolidated the `CreateTranslation` and `UpdateTranslation` commands into a single, more robust `CreateOrUpdateTranslation` command. This uses a database-level `Upsert` for atomicity.
- Centralized authorization for translatable entities into a new `CanEditEntity` check within the application service layer.
- Fixed a critical bug in the `MergeWork` command that caused a UNIQUE constraint violation when merging works with conflicting translations. The logic now intelligently handles language conflicts.
- Implemented decrementing for "like" counts in the analytics service when a like is deleted, ensuring accurate statistics.
- Stabilized the test suite by switching to a file-based database for integration tests, fixing test data isolation issues, and adding a unique index to the `Translation` model to enforce data integrity.
- Refactored manual mocks to use the `testify/mock` library for better consistency and maintainability.
2025-10-05 09:41:40 +00:00

113 lines
3.2 KiB
Go

package translation
import (
"context"
"fmt"
"tercul/internal/app/authz"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// TranslationCommands contains the command handlers for the translation aggregate.
type TranslationCommands struct {
repo domain.TranslationRepository
authzSvc *authz.Service
tracer trace.Tracer
}
// NewTranslationCommands creates a new TranslationCommands handler.
func NewTranslationCommands(repo domain.TranslationRepository, authzSvc *authz.Service) *TranslationCommands {
return &TranslationCommands{
repo: repo,
authzSvc: authzSvc,
tracer: otel.Tracer("translation.commands"),
}
}
// CreateOrUpdateTranslationInput represents the input for creating or updating a translation.
type CreateOrUpdateTranslationInput struct {
Title string
Content string
Description string
Language string
Status domain.TranslationStatus
TranslatableID uint
TranslatableType string
TranslatorID *uint
IsOriginalLanguage bool
}
// CreateOrUpdateTranslation creates a new translation or updates an existing one.
func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, input CreateOrUpdateTranslationInput) (*domain.Translation, error) {
ctx, span := c.tracer.Start(ctx, "CreateOrUpdateTranslation")
defer span.End()
// Validate input first
if input.Language == "" {
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
}
if input.TranslatableID == 0 {
return nil, fmt.Errorf("%w: translatable ID cannot be zero", domain.ErrValidation)
}
if input.TranslatableType == "" {
return nil, fmt.Errorf("%w: translatable type cannot be empty", domain.ErrValidation)
}
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
// Authorize that the user can edit the parent entity.
can, err := c.authzSvc.CanEditEntity(ctx, userID, input.TranslatableType, input.TranslatableID)
if err != nil {
return nil, fmt.Errorf("authorization check failed: %w", err)
}
if !can {
return nil, domain.ErrForbidden
}
var translatorID uint
if input.TranslatorID != nil {
translatorID = *input.TranslatorID
} else {
translatorID = userID
}
translation := &domain.Translation{
Title: input.Title,
Content: input.Content,
Description: input.Description,
Language: input.Language,
Status: input.Status,
TranslatableID: input.TranslatableID,
TranslatableType: input.TranslatableType,
TranslatorID: &translatorID,
IsOriginalLanguage: input.IsOriginalLanguage,
}
if err := c.repo.Upsert(ctx, translation); err != nil {
return nil, fmt.Errorf("failed to upsert translation: %w", err)
}
return translation, nil
}
// DeleteTranslation deletes a translation by ID.
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
defer span.End()
can, err := c.authzSvc.CanDeleteTranslation(ctx)
if err != nil {
return err
}
if !can {
return domain.ErrForbidden
}
return c.repo.Delete(ctx, id)
}