Feat[BE]: integrate document service into expense module and update related DTOs for document handling

This commit is contained in:
aguhh18
2025-12-26 11:20:57 +07:00
parent 12e5706318
commit a9037991ef
6 changed files with 150 additions and 159 deletions
+6 -7
View File
@@ -1,7 +1,6 @@
package entities
import (
"database/sql"
"time"
"gorm.io/gorm"
@@ -13,8 +12,6 @@ type Expense struct {
SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"`
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
TransactionDate time.Time `gorm:"type:date;not null"`
Notes string `gorm:"type:text;column:notes"`
@@ -23,8 +20,10 @@ type Expense struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
}
+14 -7
View File
@@ -1,7 +1,6 @@
package dto
import (
"encoding/json"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -41,8 +40,8 @@ type ExpenseListDTO struct {
type ExpenseDetailDTO struct {
ExpenseBaseDTO
Documents []DocumentDTO `json:"documents,omitempty"`
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
Documents []DocumentDTO `json:"documents"`
RealizationDocs []DocumentDTO `json:"realization_docs"`
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
TotalPengajuan float64 `json:"total_pengajuan"`
TotalRealisasi float64 `json:"total_realisasi"`
@@ -179,12 +178,20 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var pengajuans []ExpenseNonstockDTO
var realisasi []ExpenseRealizationDTO
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
// Map documents from Document service
for _, doc := range e.Documents {
documents = append(documents, DocumentDTO{
ID: uint64(doc.Id),
Path: doc.Path,
})
}
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
// Map realization documents from Document service
for _, doc := range e.RealizationDocuments {
realizationDocs = append(realizationDocs, DocumentDTO{
ID: uint64(doc.Id),
Path: doc.Path,
})
}
if len(e.Nonstocks) > 0 {
+7 -1
View File
@@ -1,6 +1,7 @@
package expenses
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
@@ -31,15 +32,20 @@ func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db)
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
// Register workflow steps for EXPENSES approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
}
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, documentSvc, validate)
userService := sUser.NewUserService(userRepo, validate)
ExpenseRoutes(router, userService, expenseService)
@@ -2,11 +2,8 @@ package service
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"mime/multipart"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -49,9 +46,10 @@ type expenseService struct {
ApprovalSvc commonSvc.ApprovalService
RealizationRepository repository.ExpenseRealizationRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
DocumentSvc commonSvc.DocumentService
}
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService {
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, validate *validator.Validate) ExpenseService {
return &expenseService{
Log: utils.Log,
Validate: validate,
@@ -61,6 +59,7 @@ func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierR
ApprovalSvc: approvalSvc,
RealizationRepository: realizationRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
DocumentSvc: documentSvc,
}
}
@@ -72,7 +71,13 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Nonstocks.Realization").
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
Preload("Nonstocks.Kandang").
Preload("Nonstocks.Kandang.Location")
Preload("Nonstocks.Kandang.Location").
Preload("Documents", func(db *gorm.DB) *gorm.DB {
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpense))
}).
Preload("RealizationDocuments", func(db *gorm.DB) *gorm.DB {
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpenseRealization))
})
}
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) {
@@ -269,9 +274,23 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, uint(expense.Id), req.Documents, false); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpense),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpense),
DocumentableID: expense.Id,
CreatedBy: &createdByUint,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
}
}
@@ -527,9 +546,23 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, id, req.Documents, false); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpense),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpense),
DocumentableID: uint64(id),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
}
}
@@ -658,9 +691,24 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpenseRealization),
Index: &idx,
})
}
actorID := uint(1) // TODO: replace with authenticated user id
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
DocumentableID: uint64(expenseID),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
}
}
@@ -833,9 +881,24 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
}
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpenseRealization),
Index: &idx,
})
}
actorID := uint(1) // TODO: replace with authenticated user id
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
DocumentableID: uint64(expenseID),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
}
}
@@ -870,79 +933,6 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
return responseDTO, nil
}
func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx repository.ExpenseRepository, expenseID uint, documents []*multipart.FileHeader, isRealization bool) error {
if len(documents) == 0 {
return nil
}
var existingDocuments []expenseDto.DocumentDTO
var fieldName string
if isRealization {
fieldName = "realization_document_path"
} else {
fieldName = "document_path"
}
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document processing")
}
} else {
var documentField sql.NullString
if isRealization {
documentField = expense.RealizationDocumentPath
} else {
documentField = expense.DocumentPath
}
if documentField.Valid && documentField.String != "" {
if err := json.Unmarshal([]byte(documentField.String), &existingDocuments); err != nil {
existingDocuments = []expenseDto.DocumentDTO{}
}
}
}
var startID uint64 = 1
if len(existingDocuments) > 0 {
maxID := uint64(0)
for _, doc := range existingDocuments {
if doc.ID > maxID {
maxID = doc.ID
}
}
startID = maxID + 1
}
for i, doc := range documents {
documentPath := doc.Filename
document := expenseDto.DocumentDTO{
ID: startID + uint64(i),
Path: documentPath,
}
existingDocuments = append(existingDocuments, document)
}
documentJSON, err := json.Marshal(existingDocuments)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
fieldName: string(documentJSON),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
return nil
}
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
if err := commonSvc.EnsureRelations(ctx.Context(),
@@ -951,62 +941,40 @@ func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, document
return err
}
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
expenseRepoTx := repository.NewExpenseRepository(tx)
if s.DocumentSvc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document deletion")
// Verify document exists and belongs to the expense
var documentableType string
if isRealization {
documentableType = string(utils.DocumentableTypeExpenseRealization)
} else {
documentableType = string(utils.DocumentableTypeExpense)
}
documents, err := s.DocumentSvc.ListByTarget(ctx.Context(), documentableType, uint64(expenseID))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve documents")
}
documentFound := false
var documentIDsToDelete []uint
for _, doc := range documents {
if uint64(doc.Id) == documentID {
documentFound = true
documentIDsToDelete = append(documentIDsToDelete, doc.Id)
break
}
}
var existingDocuments []expenseDto.DocumentDTO
var fieldName string
if !documentFound {
return fiber.NewError(fiber.StatusNotFound, "Document not found")
}
if isRealization {
fieldName = "realization_document_path"
if expense.RealizationDocumentPath.Valid && expense.RealizationDocumentPath.String != "" {
if err := json.Unmarshal([]byte(expense.RealizationDocumentPath.String), &existingDocuments); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing realization documents")
}
}
} else {
fieldName = "document_path"
if expense.DocumentPath.Valid && expense.DocumentPath.String != "" {
if err := json.Unmarshal([]byte(expense.DocumentPath.String), &existingDocuments); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing documents")
}
}
}
var updatedDocuments []expenseDto.DocumentDTO
documentFound := false
for _, doc := range existingDocuments {
if doc.ID == documentID {
documentFound = true
continue
}
updatedDocuments = append(updatedDocuments, doc)
}
if !documentFound {
return fiber.NewError(fiber.StatusNotFound, "Document not found")
}
documentJSON, err := json.Marshal(updatedDocuments)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
fieldName: string(documentJSON),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
return nil
}); err != nil {
return err
// Delete document from database and storage
if err := s.DocumentSvc.DeleteDocuments(ctx.Context(), documentIDsToDelete, true); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete document")
}
return nil
+7
View File
@@ -1,6 +1,7 @@
package purchases
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
@@ -41,6 +42,11 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
documentRepo := commonRepo.NewDocumentRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
}
@@ -54,6 +60,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalService,
expenseRealizationRepo,
projectFlockKandangRepository,
documentSvc,
validate,
)
expenseBridge := service.NewExpenseBridge(
+6 -2
View File
@@ -324,9 +324,13 @@ type DocumentType string
type DocumentableType string
const (
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
)
// -------------------------------------------------------------------