diff --git a/internal/entities/expense.go b/internal/entities/expense.go index e6ab1d77..83a6031b 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -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"` } diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index c55dba2c..4bb9ebe1 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -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 { diff --git a/internal/modules/expenses/module.go b/internal/modules/expenses/module.go index 6d276b5d..b495b5b9 100644 --- a/internal/modules/expenses/module.go +++ b/internal/modules/expenses/module.go @@ -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) diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 24ba4f2e..728c689f 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -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 diff --git a/internal/modules/purchases/module.go b/internal/modules/purchases/module.go index ec1b24f7..6daf2a39 100644 --- a/internal/modules/purchases/module.go +++ b/internal/modules/purchases/module.go @@ -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( diff --git a/internal/utils/constant.go b/internal/utils/constant.go index db5598e5..85b0cc91 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -408,9 +408,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" ) // -------------------------------------------------------------------