update purchase triger to expense

This commit is contained in:
ragilap
2025-12-06 21:09:23 +07:00
parent 2d3f7f7ef9
commit a586fe3781
3 changed files with 91 additions and 65 deletions
+57 -58
View File
@@ -3,7 +3,7 @@ package middleware
import ( import (
"strings" "strings"
// "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -32,65 +32,65 @@ type AuthContext struct {
// fine-grained authorization using the SSO access token scopes. // fine-grained authorization using the SSO access token scopes.
func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler { func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
// token := bearerToken(c) token := bearerToken(c)
// if token == "" { if token == "" {
// token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
// } }
// if token == "" { if token == "" {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// verification, err := sso.VerifyAccessToken(token) verification, err := sso.VerifyAccessToken(token)
// if err != nil { if err != nil {
// utils.Log.WithError(err).Warn("auth: token verification failed") utils.Log.WithError(err).Warn("auth: token verification failed")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// if verification.UserID == 0 { if verification.UserID == 0 {
// return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
// } }
// if err := ensureNotRevoked(c, token, verification); err != nil { if err := ensureNotRevoked(c, token, verification); err != nil {
// return err return err
// } }
// user, err := userService.GetBySSOUserID(c, verification.UserID) user, err := userService.GetBySSOUserID(c, verification.UserID)
// if err != nil || user == nil { if err != nil || user == nil {
// utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// if len(requiredScopes) > 0 { if len(requiredScopes) > 0 {
// if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
// } }
// } }
// var roles []sso.Role var roles []sso.Role
// permissions := make(map[string]struct{}) permissions := make(map[string]struct{})
// if verification.UserID != 0 { if verification.UserID != 0 {
// if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
// utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
// } else if profile != nil { } else if profile != nil {
// roles = profile.Roles roles = profile.Roles
// for _, perm := range profile.PermissionNames() { for _, perm := range profile.PermissionNames() {
// if perm != "" { if perm != "" {
// permissions[perm] = struct{}{} permissions[perm] = struct{}{}
// } }
// } }
// } }
// } }
// ctx := &AuthContext{ ctx := &AuthContext{
// Token: token, Token: token,
// Verification: verification, Verification: verification,
// User: user, User: user,
// Roles: roles, Roles: roles,
// Permissions: permissions, Permissions: permissions,
// } }
// c.Locals(authContextLocalsKey, ctx) c.Locals(authContextLocalsKey, ctx)
// c.Locals(authUserLocalsKey, user) c.Locals(authUserLocalsKey, user)
return c.Next() return c.Next()
} }
@@ -106,12 +106,11 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
} }
func ActorIDFromContext(c *fiber.Ctx) (uint, error) { func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
// user, ok := AuthenticatedUser(c) user, ok := AuthenticatedUser(c)
// if !ok || user == nil || user.Id == 0 { if !ok || user == nil || user.Id == 0 {
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// return user.Id, nil return user.Id, nil
return 1, nil
} }
// AuthDetails returns the full authentication context (token, claims, user). // AuthDetails returns the full authentication context (token, claims, user).
@@ -22,13 +22,11 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// PurchaseExpenseBridge allows purchase flows to sync expense data on receiving/deletion.
type PurchaseExpenseBridge interface { type PurchaseExpenseBridge interface {
OnItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error OnItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error
} }
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
type ExpenseReceivingPayload struct { type ExpenseReceivingPayload struct {
PurchaseItemID uint PurchaseItemID uint
ProductID uint ProductID uint
@@ -51,7 +49,6 @@ func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID) 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 { type expenseBridge struct {
db *gorm.DB db *gorm.DB
purchaseRepo rPurchase.PurchaseRepository purchaseRepo rPurchase.PurchaseRepository
@@ -158,7 +155,6 @@ func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []enti
}) })
} }
// cleanupExistingNonstocks deletes expense_nonstocks (and their approvals/expenses if empty) for the given payloads.
func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []ExpenseReceivingPayload) error { func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []ExpenseReceivingPayload) error {
if len(updates) == 0 { if len(updates) == 0 {
return nil return nil
@@ -236,7 +232,6 @@ func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []
}) })
} }
// cleanupEmptyExpenses deletes expense headers (and approvals) that have no nonstocks.
func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []uint64) error { func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []uint64) error {
if len(expenseIDs) == 0 { if len(expenseIDs) == 0 {
return nil return nil
@@ -263,6 +258,23 @@ func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []u
}) })
} }
func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
if len(expenseIDs) == 0 {
return nil
}
if actorID == 0 {
actorID = 1
}
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
action := entity.ApprovalActionUpdated
for id := range expenseIDs {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
return nil
}
func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error { func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
if purchaseID == 0 || len(updates) == 0 { if purchaseID == 0 || len(updates) == 0 {
return nil return nil
@@ -292,6 +304,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
itemLinks := make(map[uint]itemLink) itemLinks := make(map[uint]itemLink)
existingExpenseByKey := make(map[string]uint64) existingExpenseByKey := make(map[string]uint64)
updatedExpenses := make(map[uint64]struct{})
if len(updates) > 0 { if len(updates) > 0 {
ids := make([]uint, 0, len(updates)) ids := make([]uint, 0, len(updates))
for _, upd := range updates { for _, upd := range updates {
@@ -407,6 +420,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
// Track cleanup for old header if it becomes empty. // Track cleanup for old header if it becomes empty.
movedFrom = append(movedFrom, link.ExpenseID) movedFrom = append(movedFrom, link.ExpenseID)
existingExpenseByKey[newKey] = targetExpenseID existingExpenseByKey[newKey] = targetExpenseID
updatedExpenses[targetExpenseID] = struct{}{}
handledUpdate = true handledUpdate = true
} else { } else {
requiresDelete = true requiresDelete = true
@@ -426,6 +440,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
}).Error; err != nil { }).Error; err != nil {
return err return err
} }
if link.ExpenseID != 0 {
updatedExpenses[link.ExpenseID] = struct{}{}
}
handledUpdate = true handledUpdate = true
} }
} }
@@ -464,6 +481,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
kandangID: kandangID, kandangID: kandangID,
totalPrice: totalPrice, 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. // For payloads that require delete/recreate, clean up their old links first.
@@ -541,6 +561,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil { if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil {
return err return err
} }
if expenseDetail != nil && expenseDetail.Id != 0 {
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
}
} }
// Cleanup old expense headers that became empty after re-link. // Cleanup old expense headers that became empty after re-link.
@@ -550,6 +573,12 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
} }
} }
if len(updatedExpenses) > 0 {
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
return err
}
}
return nil return nil
} }
@@ -996,10 +996,8 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
} }
itemIDs := make([]uint, 0, len(purchase.Items))
itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items)) itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items))
for i, item := range purchase.Items { for i, item := range purchase.Items {
itemIDs = append(itemIDs, item.Id)
itemsToDelete[i] = item itemsToDelete[i] = item
} }