mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-21 13:55:43 +00:00
704 lines
20 KiB
Go
704 lines
20 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"
|
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
)
|
|
|
|
// PurchaseExpenseBridge allows purchase flows to sync expense data on receiving/deletion.
|
|
type PurchaseExpenseBridge interface {
|
|
OnItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
|
|
OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error
|
|
}
|
|
|
|
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
|
|
type ExpenseReceivingPayload struct {
|
|
PurchaseItemID uint
|
|
ProductID uint
|
|
WarehouseID uint
|
|
SupplierID uint
|
|
TransportPerItem *float64
|
|
ReceivedQty float64
|
|
ReceivedDate *time.Time
|
|
}
|
|
|
|
type groupedItem struct {
|
|
item *entity.PurchaseItem
|
|
payload ExpenseReceivingPayload
|
|
projectFK *uint
|
|
kandangID *uint
|
|
totalPrice float64
|
|
}
|
|
|
|
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
|
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
|
|
}
|
|
|
|
// expenseBridge is the real implementation that syncs purchases to expenses on receiving/deletion.
|
|
type expenseBridge struct {
|
|
db *gorm.DB
|
|
purchaseRepo rPurchase.PurchaseRepository
|
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
|
expenseSvc expenseSvc.ExpenseService
|
|
}
|
|
|
|
func NewExpenseBridge(
|
|
db *gorm.DB,
|
|
purchaseRepo rPurchase.PurchaseRepository,
|
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
|
expenseSvc expenseSvc.ExpenseService,
|
|
) PurchaseExpenseBridge {
|
|
return &expenseBridge{
|
|
db: db,
|
|
purchaseRepo: purchaseRepo,
|
|
projectFlockKandangRepo: projectFlockKandangRepo,
|
|
expenseSvc: expenseSvc,
|
|
}
|
|
}
|
|
|
|
func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []entity.PurchaseItem) 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
|
|
}
|
|
}
|
|
|
|
var links []struct {
|
|
ItemID uint
|
|
ExpenseNonstockID *uint64
|
|
}
|
|
if err := tx.
|
|
Model(&entity.PurchaseItem{}).
|
|
Select("id as item_id, expense_nonstock_id").
|
|
Where("id IN ?", extractIDs(items)).
|
|
Scan(&links).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, link := range links {
|
|
if link.ExpenseNonstockID == nil || *link.ExpenseNonstockID == 0 {
|
|
continue
|
|
}
|
|
var expenseID uint64
|
|
if err := tx.
|
|
Model(&entity.ExpenseNonstock{}).
|
|
Select("expense_id").
|
|
Where("id = ?", *link.ExpenseNonstockID).
|
|
Scan(&expenseID).Error; err != nil {
|
|
return err
|
|
}
|
|
if expenseID != 0 {
|
|
expenseIDs[expenseID] = struct{}{}
|
|
}
|
|
if err := tx.Delete(&entity.ExpenseNonstock{}, *link.ExpenseNonstockID).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
|
|
})
|
|
}
|
|
|
|
// cleanupExistingNonstocks deletes expense_nonstocks (and their approvals/expenses if empty) for the given payloads.
|
|
func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []ExpenseReceivingPayload) error {
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
itemIDs := make([]uint, 0, len(updates))
|
|
for _, upd := range updates {
|
|
if upd.PurchaseItemID != 0 {
|
|
itemIDs = append(itemIDs, upd.PurchaseItemID)
|
|
}
|
|
}
|
|
if len(itemIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var links []struct {
|
|
ItemID uint
|
|
ExpenseNonstockID *uint64
|
|
}
|
|
if err := tx.Model(&entity.PurchaseItem{}).
|
|
Select("id as item_id, expense_nonstock_id").
|
|
Where("id IN ?", itemIDs).
|
|
Scan(&links).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
expenseIDs := make(map[uint64]struct{})
|
|
expenseNonstockIDs := make([]uint64, 0)
|
|
for _, link := range links {
|
|
if link.ExpenseNonstockID != nil && *link.ExpenseNonstockID != 0 {
|
|
expenseNonstockIDs = append(expenseNonstockIDs, *link.ExpenseNonstockID)
|
|
}
|
|
}
|
|
|
|
if len(expenseNonstockIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|
|
|
|
// cleanupEmptyExpenses deletes expense headers (and approvals) that have no nonstocks.
|
|
func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []uint64) error {
|
|
if len(expenseIDs) == 0 {
|
|
return nil
|
|
}
|
|
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
|
for _, id := range expenseIDs {
|
|
var count int64
|
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
|
Where("expense_id = ?", id).
|
|
Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(id)); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Delete(&entity.Expense{}, id).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
|
|
if purchaseID == 0 || len(updates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ctx := c.Context()
|
|
|
|
// Load current links to decide whether to update in place or recreate.
|
|
type itemLink struct {
|
|
ExpenseNonstockID uint64
|
|
ExpenseID uint64
|
|
SupplierID uint
|
|
TransactionDate time.Time
|
|
Qty float64
|
|
Price float64
|
|
}
|
|
|
|
purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB {
|
|
return db.
|
|
Preload("Items").
|
|
Preload("Items.Warehouse").
|
|
Preload("Items.Warehouse.Kandang")
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
itemLinks := make(map[uint]itemLink)
|
|
existingExpenseByKey := make(map[string]uint64)
|
|
if len(updates) > 0 {
|
|
ids := make([]uint, 0, len(updates))
|
|
for _, upd := range updates {
|
|
if upd.PurchaseItemID != 0 {
|
|
ids = append(ids, upd.PurchaseItemID)
|
|
}
|
|
}
|
|
if len(ids) > 0 {
|
|
rows := make([]struct {
|
|
ItemID uint
|
|
ExpenseNonstockID uint64
|
|
ExpenseID uint64
|
|
SupplierID uint
|
|
TransactionDate time.Time
|
|
Qty float64
|
|
Price float64
|
|
}, 0)
|
|
if err := b.db.WithContext(ctx).
|
|
Table("purchase_items pi").
|
|
Select("pi.id as item_id, en.id as expense_nonstock_id, en.expense_id, e.supplier_id, e.transaction_date, en.qty, en.price").
|
|
Joins("LEFT JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id").
|
|
Joins("LEFT JOIN expenses e ON e.id = en.expense_id").
|
|
Where("pi.id IN ?", ids).
|
|
Scan(&rows).Error; err != nil {
|
|
return err
|
|
}
|
|
// Build quick lookup per item and per group key for existing expenses.
|
|
for _, row := range rows {
|
|
itemLinks[row.ItemID] = itemLink{
|
|
ExpenseNonstockID: row.ExpenseNonstockID,
|
|
ExpenseID: row.ExpenseID,
|
|
SupplierID: row.SupplierID,
|
|
TransactionDate: row.TransactionDate,
|
|
Qty: row.Qty,
|
|
Price: row.Price,
|
|
}
|
|
if row.ExpenseID != 0 && row.SupplierID != 0 && !row.TransactionDate.IsZero() {
|
|
// Use warehouse from purchase item; if not found, skip key.
|
|
for i := range purchase.Items {
|
|
if purchase.Items[i].Id == row.ItemID {
|
|
key := groupingKey(row.SupplierID, row.TransactionDate.UTC().Truncate(24*time.Hour), purchase.Items[i].WarehouseId)
|
|
existingExpenseByKey[key] = row.ExpenseID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
|
for i := range purchase.Items {
|
|
itemMap[purchase.Items[i].Id] = &purchase.Items[i]
|
|
}
|
|
|
|
groups := make(map[string][]groupedItem)
|
|
toRecreate := make([]ExpenseReceivingPayload, 0)
|
|
|
|
movedFrom := make([]uint64, 0)
|
|
|
|
for _, payload := range updates {
|
|
if payload.ReceivedDate == nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
|
}
|
|
item := itemMap[payload.PurchaseItemID]
|
|
if item == nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
|
}
|
|
if payload.ReceivedQty <= 0 {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d must be greater than 0", payload.PurchaseItemID))
|
|
}
|
|
|
|
receivedDate := payload.ReceivedDate.UTC().Truncate(24 * time.Hour)
|
|
supplierID := payload.SupplierID
|
|
if supplierID == 0 {
|
|
supplierID = purchase.SupplierId
|
|
}
|
|
|
|
// Decide whether to update existing expense_nonstock or recreate.
|
|
link, hasLink := itemLinks[payload.PurchaseItemID]
|
|
requiresDelete := false
|
|
handledUpdate := false
|
|
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
|
oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
|
|
newDate := receivedDate
|
|
oldSupplier := link.SupplierID
|
|
pricePerItem := item.Price
|
|
if payload.TransportPerItem != nil {
|
|
pricePerItem = *payload.TransportPerItem
|
|
}
|
|
|
|
// Supplier/date change: prefer re-link to existing expense in target group; otherwise recreate.
|
|
if oldSupplier != supplierID || !oldDate.Equal(newDate) {
|
|
newKey := groupingKey(supplierID, newDate, payload.WarehouseID)
|
|
if targetExpenseID, ok := existingExpenseByKey[newKey]; ok && targetExpenseID != 0 {
|
|
// Move nonstock to existing expense header in the target group.
|
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
|
pricePerItem := item.Price
|
|
if payload.TransportPerItem != nil {
|
|
pricePerItem = *payload.TransportPerItem
|
|
}
|
|
if err := b.db.WithContext(ctx).
|
|
Model(&entity.ExpenseNonstock{}).
|
|
Where("id = ?", link.ExpenseNonstockID).
|
|
Updates(map[string]interface{}{
|
|
"expense_id": targetExpenseID,
|
|
"qty": payload.ReceivedQty,
|
|
"price": pricePerItem,
|
|
"notes": note,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
// Track cleanup for old header if it becomes empty.
|
|
movedFrom = append(movedFrom, link.ExpenseID)
|
|
existingExpenseByKey[newKey] = targetExpenseID
|
|
handledUpdate = true
|
|
} else {
|
|
requiresDelete = true
|
|
}
|
|
}
|
|
|
|
// If we reach here and no delete is required, update the existing nonstock fields and skip creation.
|
|
if !requiresDelete {
|
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
|
if err := b.db.WithContext(ctx).
|
|
Model(&entity.ExpenseNonstock{}).
|
|
Where("id = ?", link.ExpenseNonstockID).
|
|
Updates(map[string]interface{}{
|
|
"qty": payload.ReceivedQty,
|
|
"price": pricePerItem,
|
|
"notes": note,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
handledUpdate = true
|
|
}
|
|
}
|
|
|
|
if requiresDelete {
|
|
toRecreate = append(toRecreate, payload)
|
|
continue
|
|
}
|
|
if handledUpdate {
|
|
continue
|
|
}
|
|
|
|
key := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
|
|
|
var kandangID *uint
|
|
var projectFK *uint
|
|
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
|
id := uint(*item.Warehouse.KandangId)
|
|
kandangID = &id
|
|
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
|
pid := uint(project.Id)
|
|
projectFK = &pid
|
|
}
|
|
}
|
|
|
|
pricePerItem := item.Price
|
|
if payload.TransportPerItem != nil {
|
|
pricePerItem = *payload.TransportPerItem
|
|
}
|
|
totalPrice := pricePerItem * payload.ReceivedQty
|
|
|
|
groups[key] = append(groups[key], groupedItem{
|
|
item: item,
|
|
payload: payload,
|
|
projectFK: projectFK,
|
|
kandangID: kandangID,
|
|
totalPrice: totalPrice,
|
|
})
|
|
}
|
|
|
|
// For payloads that require delete/recreate, clean up their old links first.
|
|
if len(toRecreate) > 0 {
|
|
if err := b.cleanupExistingNonstocks(ctx, toRecreate); err != nil {
|
|
return err
|
|
}
|
|
// Then add them back into grouping for creation.
|
|
for _, payload := range toRecreate {
|
|
item := itemMap[payload.PurchaseItemID]
|
|
if item == nil || payload.ReceivedDate == nil {
|
|
continue
|
|
}
|
|
receivedDate := payload.ReceivedDate.UTC().Truncate(24 * time.Hour)
|
|
supplierID := payload.SupplierID
|
|
if supplierID == 0 {
|
|
supplierID = purchase.SupplierId
|
|
}
|
|
key := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
|
|
|
var kandangID *uint
|
|
var projectFK *uint
|
|
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
|
id := uint(*item.Warehouse.KandangId)
|
|
kandangID = &id
|
|
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
|
pid := uint(project.Id)
|
|
projectFK = &pid
|
|
}
|
|
}
|
|
|
|
pricePerItem := item.Price
|
|
if payload.TransportPerItem != nil {
|
|
pricePerItem = *payload.TransportPerItem
|
|
}
|
|
totalPrice := pricePerItem * payload.ReceivedQty
|
|
|
|
groups[key] = append(groups[key], groupedItem{
|
|
item: item,
|
|
payload: payload,
|
|
projectFK: projectFK,
|
|
kandangID: kandangID,
|
|
totalPrice: totalPrice,
|
|
})
|
|
}
|
|
}
|
|
|
|
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, purchase, items, expenseDate, expeditionNonstockID, purchase.PoNumber, uint(supplierID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Cleanup old expense headers that became empty after re-link.
|
|
if len(movedFrom) > 0 {
|
|
if err := b.cleanupEmptyExpenses(ctx, movedFrom); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *expenseBridge) 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 extractIDs(items []entity.PurchaseItem) []uint {
|
|
result := make([]uint, 0, len(items))
|
|
for _, item := range items {
|
|
if item.Id != 0 {
|
|
result = append(result, item.Id)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (b *expenseBridge) createExpenseViaService(
|
|
c *fiber.Ctx,
|
|
purchase *entity.Purchase,
|
|
items []groupedItem,
|
|
expenseDate time.Time,
|
|
expeditionNonstockID uint64,
|
|
poNumber *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
|
|
if kandangID == nil || *kandangID == 0 {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
|
}
|
|
|
|
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
|
for _, gi := range items {
|
|
note := fmt.Sprintf("purchase_item:%d", gi.payload.PurchaseItemID)
|
|
price := gi.item.Price
|
|
if gi.payload.TransportPerItem != nil {
|
|
price = *gi.payload.TransportPerItem
|
|
}
|
|
costItems = append(costItems, expenseValidation.CostItem{
|
|
NonstockID: expeditionNonstockID,
|
|
Quantity: gi.payload.ReceivedQty,
|
|
Price: price,
|
|
Notes: note,
|
|
})
|
|
}
|
|
|
|
req := &expenseValidation.Create{
|
|
PoNumber: "",
|
|
TransactionDate: utils.FormatDate(expenseDate),
|
|
Category: "BOP",
|
|
SupplierID: uint64(supplierID),
|
|
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
|
KandangID: uint64(*kandangID),
|
|
CostItems: costItems,
|
|
}},
|
|
}
|
|
if poNumber != nil {
|
|
req.PoNumber = *poNumber
|
|
}
|
|
|
|
detail, err := b.expenseSvc.CreateOne(c, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Mark approvals up to Finance so latest is Manager Finance
|
|
action := entity.ApprovalActionApproved
|
|
actorID := uint(purchase.CreatedBy)
|
|
if actorID == 0 {
|
|
actorID = 1
|
|
}
|
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &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 *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedItem) error {
|
|
if detail == nil || len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
noteToExpenseNonstock := make(map[uint]uint64)
|
|
for _, kandang := range detail.Kandangs {
|
|
for _, pengajuan := range kandang.Pengajuans {
|
|
note := strings.TrimSpace(pengajuan.Notes)
|
|
if note == "" {
|
|
continue
|
|
}
|
|
const prefix = "purchase_item:"
|
|
if !strings.HasPrefix(note, prefix) {
|
|
continue
|
|
}
|
|
idStr := strings.TrimPrefix(note, prefix)
|
|
var itemID uint
|
|
if _, err := fmt.Sscanf(idStr, "%d", &itemID); err != nil {
|
|
continue
|
|
}
|
|
noteToExpenseNonstock[itemID] = pengajuan.Id
|
|
}
|
|
}
|
|
|
|
if len(noteToExpenseNonstock) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, gi := range items {
|
|
expenseNonstockID, ok := noteToExpenseNonstock[gi.payload.PurchaseItemID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := b.db.WithContext(ctx).
|
|
Model(&entity.PurchaseItem{}).
|
|
Where("id = ?", gi.payload.PurchaseItemID).
|
|
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|