mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
475 lines
14 KiB
Go
475 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
|
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
|
expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
|
expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
|
kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
)
|
|
|
|
type TransferExpenseBridge interface {
|
|
OnItemsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error
|
|
OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error
|
|
}
|
|
|
|
type TransferExpenseReceivingPayload struct {
|
|
TransferDetailID uint64
|
|
ProductID uint64
|
|
WarehouseID uint64
|
|
SupplierID uint64
|
|
TransportPerItem *float64
|
|
DeliveredQty float64
|
|
DeliveredDate *time.Time
|
|
}
|
|
|
|
type groupedTransferItem struct {
|
|
detail *entity.StockTransferDetail
|
|
payload TransferExpenseReceivingPayload
|
|
projectFK *uint
|
|
kandangID *uint
|
|
totalPrice float64
|
|
shippingCostTotal float64
|
|
}
|
|
|
|
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
|
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
|
|
}
|
|
|
|
type transferExpenseBridge struct {
|
|
db *gorm.DB
|
|
transferRepo rStockTransfer.StockTransferRepository
|
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
|
kandangRepo kandangRepo.KandangRepository
|
|
expenseSvc expenseSvc.ExpenseService
|
|
}
|
|
|
|
func NewTransferExpenseBridge(
|
|
db *gorm.DB,
|
|
transferRepo rStockTransfer.StockTransferRepository,
|
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
|
kandangRepo kandangRepo.KandangRepository,
|
|
expenseSvc expenseSvc.ExpenseService,
|
|
) TransferExpenseBridge {
|
|
return &transferExpenseBridge{
|
|
db: db,
|
|
transferRepo: transferRepo,
|
|
projectFlockKandangRepo: projectFlockKandangRepo,
|
|
kandangRepo: kandangRepo,
|
|
expenseSvc: expenseSvc,
|
|
}
|
|
}
|
|
|
|
func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, items []entity.StockTransferDetail) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
expenseIDs := make(map[uint64]struct{})
|
|
expenseNonstockIDs := make([]uint64, 0)
|
|
|
|
for _, item := range items {
|
|
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
|
|
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
|
|
}
|
|
}
|
|
|
|
if len(expenseNonstockIDs) > 0 {
|
|
|
|
for _, nsID := range expenseNonstockIDs {
|
|
var expenseID uint64
|
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
|
Select("expense_id").
|
|
Where("id = ?", nsID).
|
|
Scan(&expenseID).Error; err != nil {
|
|
return err
|
|
}
|
|
if expenseID != 0 {
|
|
expenseIDs[expenseID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
|
for expenseID := range expenseIDs {
|
|
var count int64
|
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
|
Where("expense_id = ?", expenseID).
|
|
Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if count == 0 {
|
|
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
|
if len(expenseIDs) == 0 {
|
|
return nil
|
|
}
|
|
if actorID == 0 {
|
|
actorID = 1
|
|
}
|
|
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
|
svc := commonSvc.NewApprovalService(approvalRepo)
|
|
action := entity.ApprovalActionCreated
|
|
|
|
for id := range expenseIDs {
|
|
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
if latestApproval == nil {
|
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *transferExpenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) {
|
|
var id uint64
|
|
err := b.db.WithContext(ctx).
|
|
Table("nonstocks AS ns").
|
|
Select("ns.id").
|
|
Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id").
|
|
Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
|
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))).
|
|
Where("nss.supplier_id = ?", supplierID).
|
|
Order("ns.id").
|
|
Limit(1).
|
|
Scan(&id).Error
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if id == 0 {
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi")
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func (b *transferExpenseBridge) createExpenseViaService(
|
|
c *fiber.Ctx,
|
|
transfer *entity.StockTransfer,
|
|
items []groupedTransferItem,
|
|
expenseDate time.Time,
|
|
expeditionNonstockID uint64,
|
|
movementNumber string,
|
|
supplierID uint,
|
|
) (*expenseDto.ExpenseDetailDTO, error) {
|
|
ctx := c.Context()
|
|
if b.expenseSvc == nil {
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available")
|
|
}
|
|
if len(items) == 0 {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense")
|
|
}
|
|
|
|
kandangID := items[0].kandangID
|
|
var locationID uint64
|
|
var expenseKandangID *uint64
|
|
if kandangID != nil && *kandangID != 0 {
|
|
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
|
|
return db.Select("id, location_id")
|
|
})
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
|
|
}
|
|
if kandang == nil {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
|
|
}
|
|
locationID = uint64(kandang.LocationId)
|
|
id := uint64(*kandangID)
|
|
expenseKandangID = &id
|
|
} else {
|
|
|
|
if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense")
|
|
}
|
|
locationID = uint64(*transfer.ToWarehouse.LocationId)
|
|
}
|
|
|
|
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
|
for _, gi := range items {
|
|
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
|
|
|
|
price := gi.shippingCostTotal
|
|
if gi.payload.TransportPerItem != nil {
|
|
price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
|
|
}
|
|
|
|
costItems = append(costItems, expenseValidation.CostItem{
|
|
NonstockID: expeditionNonstockID,
|
|
Quantity: 1,
|
|
Price: price,
|
|
Notes: note,
|
|
})
|
|
}
|
|
|
|
req := &expenseValidation.Create{
|
|
PoNumber: "",
|
|
TransactionDate: utils.FormatDate(expenseDate),
|
|
Category: string(utils.ExpenseCategoryBOP),
|
|
SupplierID: uint64(supplierID),
|
|
LocationID: locationID,
|
|
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
|
KandangID: expenseKandangID,
|
|
CostItems: costItems,
|
|
}},
|
|
}
|
|
|
|
detail, err := b.expenseSvc.CreateOne(c, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
action := entity.ApprovalActionApproved
|
|
actorID := uint(transfer.CreatedBy)
|
|
if actorID == 0 {
|
|
actorID = 1
|
|
}
|
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return detail, nil
|
|
}
|
|
|
|
func (b *transferExpenseBridge) linkExpenseNonstocksToDetails(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedTransferItem) error {
|
|
if detail == nil || len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
noteToExpenseNonstock := mapExpenseNotesForTransfer(detail)
|
|
|
|
if len(noteToExpenseNonstock) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, gi := range items {
|
|
expenseNonstockID, ok := noteToExpenseNonstock[gi.detail.Id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := b.db.WithContext(ctx).
|
|
Model(&entity.StockTransferDetail{}).
|
|
Where("id = ?", gi.detail.Id).
|
|
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func mapExpenseNotesForTransfer(detail *expenseDto.ExpenseDetailDTO) map[uint64]uint64 {
|
|
result := make(map[uint64]uint64)
|
|
if detail == nil {
|
|
return result
|
|
}
|
|
for _, kandang := range detail.Kandangs {
|
|
for _, pengajuan := range kandang.Pengajuans {
|
|
note := strings.TrimSpace(pengajuan.Notes)
|
|
if note == "" {
|
|
continue
|
|
}
|
|
const prefix = "stock_transfer_detail:"
|
|
if !strings.HasPrefix(note, prefix) {
|
|
continue
|
|
}
|
|
idStr := strings.TrimPrefix(note, prefix)
|
|
var detailID uint64
|
|
if _, err := fmt.Sscanf(idStr, "%d", &detailID); err != nil {
|
|
continue
|
|
}
|
|
result[detailID] = pengajuan.Id
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error {
|
|
if transferID == 0 || len(updates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ctx := c.Context()
|
|
|
|
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
|
|
return db.
|
|
Preload("Details").
|
|
Preload("Details.Product").
|
|
Preload("Details.DestProductWarehouse").
|
|
Preload("Details.DeliveryItems").
|
|
Preload("Details.DeliveryItems.StockTransferDelivery").
|
|
Preload("ToWarehouse")
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details))
|
|
shippingCostMap := make(map[uint64]float64) // detailID -> ShippingCostTotal
|
|
|
|
for i := range transfer.Details {
|
|
detailMap[transfer.Details[i].Id] = &transfer.Details[i]
|
|
|
|
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
|
|
if deliveryItem.StockTransferDelivery != nil {
|
|
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
groups := make(map[string][]groupedTransferItem)
|
|
|
|
for _, payload := range updates {
|
|
if payload.DeliveredDate == nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, "delivered_date is required")
|
|
}
|
|
detail := detailMap[payload.TransferDetailID]
|
|
if detail == nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer detail %d not found", payload.TransferDetailID))
|
|
}
|
|
if payload.DeliveredQty <= 0 {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Delivered quantity for detail %d must be greater than 0", payload.TransferDetailID))
|
|
}
|
|
|
|
deliveredDate := payload.DeliveredDate.UTC().Truncate(24 * time.Hour)
|
|
supplierID := payload.SupplierID
|
|
if supplierID == 0 {
|
|
supplierID = 1 // Default supplier
|
|
}
|
|
|
|
var kandangID *uint
|
|
var projectFK *uint
|
|
if detail.DestProductWarehouse.WarehouseId != 0 {
|
|
var kandangIDResult uint
|
|
if err := b.db.WithContext(ctx).
|
|
Table("warehouses").
|
|
Select("kandang_id").
|
|
Where("id = ?", detail.DestProductWarehouse.WarehouseId).
|
|
Scan(&kandangIDResult).Error; err == nil && kandangIDResult != 0 {
|
|
id := uint(kandangIDResult)
|
|
kandangID = &id
|
|
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, kandangIDResult); err == nil && project != nil {
|
|
pid := uint(project.Id)
|
|
projectFK = &pid
|
|
}
|
|
}
|
|
}
|
|
|
|
shippingCostTotal := shippingCostMap[detail.Id]
|
|
|
|
totalPrice := shippingCostTotal
|
|
if payload.TransportPerItem != nil {
|
|
|
|
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
|
|
}
|
|
|
|
warehouseID := uint(payload.WarehouseID)
|
|
if warehouseID == 0 && transfer.ToWarehouse != nil {
|
|
warehouseID = uint(transfer.ToWarehouse.Id)
|
|
}
|
|
if warehouseID == 0 && detail.DestProductWarehouse != nil {
|
|
warehouseID = uint(detail.DestProductWarehouse.WarehouseId)
|
|
}
|
|
|
|
key := groupingKey(uint(supplierID), deliveredDate, warehouseID)
|
|
groups[key] = append(groups[key], groupedTransferItem{
|
|
detail: detail,
|
|
payload: payload,
|
|
projectFK: projectFK,
|
|
kandangID: kandangID,
|
|
totalPrice: totalPrice,
|
|
shippingCostTotal: shippingCostTotal,
|
|
})
|
|
}
|
|
|
|
updatedExpenses := make(map[uint64]struct{})
|
|
|
|
for key, items := range groups {
|
|
if len(items) == 0 {
|
|
continue
|
|
}
|
|
parts := strings.Split(key, ":")
|
|
if len(parts) < 3 {
|
|
return errors.New("invalid expense grouping key")
|
|
}
|
|
expenseDate, err := utils.ParseDateString(parts[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
supplierID, err := strconv.ParseUint(parts[0], 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
expenseDetail, err := b.createExpenseViaService(c, transfer, items, expenseDate, expeditionNonstockID, transfer.MovementNumber, uint(supplierID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := b.linkExpenseNonstocksToDetails(ctx, expenseDetail, items); err != nil {
|
|
return err
|
|
}
|
|
if expenseDetail != nil && expenseDetail.Id != 0 {
|
|
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if len(updatedExpenses) > 0 {
|
|
actorID := uint(1) // Default actor
|
|
if err := b.markExpensesUpdated(ctx, updatedExpenses, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|