feat/BE/US-278/TASK-288,289-adjust schema database,Create trigger in expense module, add filter in warehouse linked to project flock

This commit is contained in:
ragilap
2025-12-08 01:23:21 +07:00
parent 4638fba318
commit 0a18753dde
10 changed files with 322 additions and 195 deletions
@@ -303,7 +303,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
}
itemLinks := make(map[uint]itemLink)
existingExpenseByKey := make(map[string]uint64)
updatedExpenses := make(map[uint64]struct{})
if len(updates) > 0 {
ids := make([]uint, 0, len(updates))
@@ -341,16 +340,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
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
}
}
}
}
}
}
@@ -361,9 +350,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
}
groups := make(map[string][]groupedItem)
toRecreate := make([]ExpenseReceivingPayload, 0)
movedFrom := make([]uint64, 0)
for _, payload := range updates {
if payload.ReceivedDate == nil {
@@ -383,10 +369,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
supplierID = purchase.SupplierId
}
// Decide whether to update existing expense_nonstock or recreate.
// Decide whether to update existing expense_nonstock or create new.
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
@@ -396,39 +380,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
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
updatedExpenses[targetExpenseID] = struct{}{}
handledUpdate = true
} else {
requiresDelete = true
}
}
// If we reach here and no delete is required, update the existing nonstock fields and skip creation.
if !requiresDelete {
// If supplier/date unchanged, update nonstock in place.
if oldSupplier == supplierID && oldDate.Equal(newDate) {
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}).
@@ -443,19 +396,139 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
if link.ExpenseID != 0 {
updatedExpenses[link.ExpenseID] = struct{}{}
}
handledUpdate = true
continue
}
// Supplier/date changed: if the linked expense has only this nonstock, update it in place.
if link.ExpenseID != 0 {
var cnt int64
if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}).
Where("expense_id = ?", link.ExpenseID).
Count(&cnt).Error; err != nil {
return err
}
if cnt == 1 {
if item.Warehouse == nil || item.Warehouse.KandangId == nil || *item.Warehouse.KandangId == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
}
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
if err != nil {
return err
}
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
if err := b.db.WithContext(ctx).
Model(&entity.Expense{}).
Where("id = ?", link.ExpenseID).
Updates(map[string]interface{}{
"transaction_date": newDate,
"supplier_id": supplierID,
}).Error; err != nil {
return err
}
updateBody := map[string]interface{}{
"qty": payload.ReceivedQty,
"price": pricePerItem,
"notes": note,
"nonstock_id": newNonstockID,
"kandang_id": uint64(*item.Warehouse.KandangId),
}
if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}).
Where("id = ?", link.ExpenseNonstockID).
Updates(updateBody).Error; err != nil {
return err
}
updatedExpenses[link.ExpenseID] = struct{}{}
continue
}
// Expense has multiple nonstocks: create new expense header for this item, then move existing nonstock to it.
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
gItem := groupedItem{
item: item,
payload: payload,
projectFK: projectFK,
kandangID: kandangID,
totalPrice: totalPrice,
}
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
if err != nil {
return err
}
expenseDetail, err := b.createExpenseViaService(c, purchase, []groupedItem{gItem}, newDate, newNonstockID, purchase.PoNumber, supplierID)
if err != nil {
return err
}
var createdNonstockID uint64
if expenseDetail != nil {
noteMap := mapExpenseNotes(expenseDetail)
createdNonstockID = noteMap[payload.PurchaseItemID]
}
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
updateBody := map[string]interface{}{
"expense_id": expenseDetail.Id,
"qty": payload.ReceivedQty,
"price": pricePerItem,
"notes": note,
"nonstock_id": newNonstockID,
}
if kandangID != nil {
updateBody["kandang_id"] = uint64(*kandangID)
}
if projectFK != nil {
updateBody["project_flock_kandang_id"] = uint64(*projectFK)
}
if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}).
Where("id = ?", link.ExpenseNonstockID).
Updates(updateBody).Error; err != nil {
return err
}
if createdNonstockID != 0 {
if err := b.db.WithContext(ctx).Delete(&entity.ExpenseNonstock{}, createdNonstockID).Error; err != nil {
return err
}
}
if link.ExpenseID != 0 {
updatedExpenses[link.ExpenseID] = struct{}{}
}
if expenseDetail != nil && expenseDetail.Id != 0 {
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
}
continue
}
// Otherwise create new expense/nonstock in grouping flow.
}
if requiresDelete {
toRecreate = append(toRecreate, payload)
continue
baseKey := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
key := baseKey
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
key = fmt.Sprintf("%s:%d", baseKey, payload.PurchaseItemID)
}
if handledUpdate {
continue
}
key := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
var kandangID *uint
var projectFK *uint
@@ -481,54 +554,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
kandangID: kandangID,
totalPrice: totalPrice,
})
if existingID, ok := existingExpenseByKey[key]; ok && existingID != 0 {
updatedExpenses[existingID] = struct{}{}
}
}
// 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 {
@@ -536,7 +561,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
continue
}
parts := strings.Split(key, ":")
if len(parts) != 3 {
if len(parts) < 3 {
return errors.New("invalid expense grouping key")
}
expenseDate, err := utils.ParseDateString(parts[1])
@@ -566,13 +591,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
}
}
// Cleanup old expense headers that became empty after re-link.
if len(movedFrom) > 0 {
if err := b.cleanupEmptyExpenses(ctx, movedFrom); err != nil {
return err
}
}
if len(updatedExpenses) > 0 {
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
return err
@@ -691,25 +709,7 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
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
}
}
noteToExpenseNonstock := mapExpenseNotes(detail)
if len(noteToExpenseNonstock) == 0 {
return nil
@@ -730,3 +730,29 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
return nil
}
func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 {
result := make(map[uint]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 = "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
}
result[itemID] = pengajuan.Id
}
}
return result
}