215 lines
6.1 KiB
Go
215 lines
6.1 KiB
Go
package services
|
|
|
|
import (
|
|
"FamilyHub/src/domain"
|
|
"FamilyHub/src/repositories"
|
|
"FamilyHub/src/utils"
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
)
|
|
|
|
type TransactionService interface {
|
|
Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error)
|
|
GetByID(ctx context.Context, id int64) (*domain.Transaction, error)
|
|
List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error)
|
|
Analytics(ctx context.Context, filter domain.TransactionAnalyticsFilter) (domain.TransactionAnalytics, error)
|
|
Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error)
|
|
Delete(ctx context.Context, id int64) error
|
|
}
|
|
|
|
type transactionService struct {
|
|
repo repositories.TransactionRepository
|
|
activityRepo repositories.ActivityRepository
|
|
}
|
|
|
|
func NewTransactionService(repo repositories.TransactionRepository, activityRepo repositories.ActivityRepository) TransactionService {
|
|
return &transactionService{repo: repo, activityRepo: activityRepo}
|
|
}
|
|
|
|
var (
|
|
ErrTransactionNotFound = errors.New("transaction not found")
|
|
ErrTransactionPatch = errors.New("empty update payload")
|
|
ErrReceiptLinkConflict = errors.New("receipt_id and detach_receipt cannot be used together")
|
|
ErrReceiptAlreadyExists = errors.New("receipt already exists")
|
|
ErrInvalidTransaction = errors.New("type and category are required")
|
|
ErrReceiptNotFound = errors.New("receipt not found")
|
|
ErrInvalidAnalytics = errors.New("type must be income or expense")
|
|
)
|
|
|
|
func (s *transactionService) Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error) {
|
|
log.Printf("transaction create request: payload=%s", utils.ToLogJSON(req))
|
|
|
|
if strings.TrimSpace(req.Type) == "" || strings.TrimSpace(req.Category) == "" {
|
|
log.Printf("transaction create failed: err=%v payload=%s", ErrInvalidTransaction, utils.ToLogJSON(req))
|
|
return nil, ErrInvalidTransaction
|
|
}
|
|
|
|
transaction := &domain.Transaction{
|
|
FamilyID: req.FamilyID,
|
|
Description: req.Description,
|
|
Type: req.Type,
|
|
DateTime: req.DateTime,
|
|
Category: req.Category,
|
|
Amount: req.Amount,
|
|
CreatedBy: req.CreatedBy,
|
|
ReceiptID: req.ReceiptID,
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, transaction); err != nil {
|
|
if errors.Is(err, repositories.ErrReceiptNotFound) {
|
|
log.Printf("transaction create failed: err=%v payload=%s", ErrReceiptNotFound, utils.ToLogJSON(req))
|
|
return nil, ErrReceiptNotFound
|
|
}
|
|
log.Printf("transaction create failed: err=%v payload=%s", err, utils.ToLogJSON(req))
|
|
return nil, err
|
|
}
|
|
|
|
if s.activityRepo != nil {
|
|
description := fmt.Sprintf("Created transaction %d", transaction.ID)
|
|
_ = s.activityRepo.Create(ctx, &domain.ActivityLog{
|
|
FamilyID: &transaction.FamilyID,
|
|
UserID: transaction.CreatedBy,
|
|
Action: "create",
|
|
EntityType: "transaction",
|
|
EntityID: &transaction.ID,
|
|
Description: description,
|
|
})
|
|
}
|
|
|
|
log.Printf("transaction create response: payload=%s", utils.ToLogJSON(transaction))
|
|
return transaction, nil
|
|
}
|
|
|
|
func (s *transactionService) GetByID(ctx context.Context, id int64) (*domain.Transaction, error) {
|
|
transaction, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if transaction == nil {
|
|
return nil, ErrTransactionNotFound
|
|
}
|
|
|
|
return transaction, nil
|
|
}
|
|
|
|
func (s *transactionService) List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error) {
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 50
|
|
}
|
|
if filter.Limit > 200 {
|
|
filter.Limit = 200
|
|
}
|
|
if filter.Offset < 0 {
|
|
filter.Offset = 0
|
|
}
|
|
|
|
return s.repo.List(ctx, filter)
|
|
}
|
|
|
|
func (s *transactionService) Analytics(ctx context.Context, filter domain.TransactionAnalyticsFilter) (domain.TransactionAnalytics, error) {
|
|
if filter.Type != nil {
|
|
typeValue := strings.TrimSpace(*filter.Type)
|
|
if typeValue != "income" && typeValue != "expense" {
|
|
return domain.TransactionAnalytics{}, ErrInvalidAnalytics
|
|
}
|
|
filter.Type = &typeValue
|
|
}
|
|
|
|
return s.repo.Analytics(ctx, filter)
|
|
}
|
|
|
|
func (s *transactionService) Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error) {
|
|
if req.ReceiptID != nil && req.DetachReceipt {
|
|
return nil, ErrReceiptLinkConflict
|
|
}
|
|
|
|
existing, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing == nil {
|
|
return nil, ErrTransactionNotFound
|
|
}
|
|
|
|
if req.Description == nil &&
|
|
req.Type == nil &&
|
|
req.DateTime == nil &&
|
|
req.Category == nil &&
|
|
req.Amount == nil &&
|
|
req.ReceiptID == nil &&
|
|
!req.DetachReceipt {
|
|
return nil, ErrTransactionPatch
|
|
}
|
|
|
|
updated := &domain.Transaction{
|
|
ID: existing.ID,
|
|
FamilyID: existing.FamilyID,
|
|
Description: existing.Description,
|
|
Type: existing.Type,
|
|
DateTime: existing.DateTime,
|
|
Category: existing.Category,
|
|
Amount: existing.Amount,
|
|
CreatedAt: existing.CreatedAt,
|
|
CreatedBy: existing.CreatedBy,
|
|
ReceiptID: existing.ReceiptID,
|
|
}
|
|
|
|
if req.Description != nil {
|
|
updated.Description = req.Description
|
|
}
|
|
if req.Type != nil {
|
|
updated.Type = *req.Type
|
|
}
|
|
if req.DateTime != nil {
|
|
updated.DateTime = *req.DateTime
|
|
}
|
|
if req.Category != nil {
|
|
updated.Category = *req.Category
|
|
}
|
|
if req.Amount != nil {
|
|
updated.Amount = *req.Amount
|
|
}
|
|
|
|
syncReceipt := false
|
|
if req.DetachReceipt {
|
|
updated.ReceiptID = nil
|
|
syncReceipt = true
|
|
}
|
|
if req.ReceiptID != nil {
|
|
updated.ReceiptID = req.ReceiptID
|
|
syncReceipt = true
|
|
}
|
|
|
|
if strings.TrimSpace(updated.Type) == "" || strings.TrimSpace(updated.Category) == "" {
|
|
return nil, ErrInvalidTransaction
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, updated, syncReceipt); err != nil {
|
|
if errors.Is(err, repositories.ErrReceiptNotFound) {
|
|
return nil, ErrReceiptNotFound
|
|
}
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrTransactionNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return s.repo.GetByID(ctx, id)
|
|
}
|
|
|
|
func (s *transactionService) Delete(ctx context.Context, id int64) error {
|
|
transaction, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if transaction == nil {
|
|
return ErrTransactionNotFound
|
|
}
|
|
|
|
return s.repo.Delete(ctx, id)
|
|
}
|