From a586fe37818dfd7b32fa94c43a0c539ff819738f Mon Sep 17 00:00:00 2001 From: ragilap Date: Sat, 6 Dec 2025 21:09:23 +0700 Subject: [PATCH] update purchase triger to expense --- internal/middleware/auth.go | 115 +++++++++--------- .../purchases/services/expense_bridge.go | 39 +++++- .../purchases/services/purchase.service.go | 2 - 3 files changed, 91 insertions(+), 65 deletions(-) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 4f14bb69..881c3a67 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -3,7 +3,7 @@ package middleware import ( "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" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" 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. func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler { return func(c *fiber.Ctx) error { - // token := bearerToken(c) - // if token == "" { - // token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) - // } - // if token == "" { - // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } + token := bearerToken(c) + if token == "" { + token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) + } + if token == "" { + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } - // verification, err := sso.VerifyAccessToken(token) - // if err != nil { - // utils.Log.WithError(err).Warn("auth: token verification failed") - // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } + verification, err := sso.VerifyAccessToken(token) + if err != nil { + utils.Log.WithError(err).Warn("auth: token verification failed") + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } - // if verification.UserID == 0 { - // return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") - // } + if verification.UserID == 0 { + return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") + } - // if err := ensureNotRevoked(c, token, verification); err != nil { - // return err - // } + if err := ensureNotRevoked(c, token, verification); err != nil { + return err + } - // user, err := userService.GetBySSOUserID(c, verification.UserID) - // if err != nil || user == nil { - // utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") - // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } + user, err := userService.GetBySSOUserID(c, verification.UserID) + if err != nil || user == nil { + utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } - // if len(requiredScopes) > 0 { - // if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { - // return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") - // } - // } + if len(requiredScopes) > 0 { + if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { + return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") + } + } - // var roles []sso.Role - // permissions := make(map[string]struct{}) - // if verification.UserID != 0 { - // if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { - // utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") - // } else if profile != nil { - // roles = profile.Roles - // for _, perm := range profile.PermissionNames() { - // if perm != "" { - // permissions[perm] = struct{}{} - // } - // } - // } - // } + var roles []sso.Role + permissions := make(map[string]struct{}) + if verification.UserID != 0 { + if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { + utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") + } else if profile != nil { + roles = profile.Roles + for _, perm := range profile.PermissionNames() { + if perm != "" { + permissions[perm] = struct{}{} + } + } + } + } - // ctx := &AuthContext{ - // Token: token, - // Verification: verification, - // User: user, - // Roles: roles, - // Permissions: permissions, - // } + ctx := &AuthContext{ + Token: token, + Verification: verification, + User: user, + Roles: roles, + Permissions: permissions, + } - // c.Locals(authContextLocalsKey, ctx) - // c.Locals(authUserLocalsKey, user) + c.Locals(authContextLocalsKey, ctx) + c.Locals(authUserLocalsKey, user) return c.Next() } @@ -106,12 +106,11 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) { } func ActorIDFromContext(c *fiber.Ctx) (uint, error) { - // user, ok := AuthenticatedUser(c) - // if !ok || user == nil || user.Id == 0 { - // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } - // return user.Id, nil - return 1, nil + user, ok := AuthenticatedUser(c) + if !ok || user == nil || user.Id == 0 { + return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } + return user.Id, nil } // AuthDetails returns the full authentication context (token, claims, user). diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 3a72a9b4..f7bf8433 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -22,13 +22,11 @@ import ( "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 @@ -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) } -// expenseBridge is the real implementation that syncs purchases to expenses on receiving/deletion. type expenseBridge struct { db *gorm.DB 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 { if len(updates) == 0 { 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 { if len(expenseIDs) == 0 { 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 { if purchaseID == 0 || len(updates) == 0 { return nil @@ -292,6 +304,7 @@ 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)) 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. movedFrom = append(movedFrom, link.ExpenseID) existingExpenseByKey[newKey] = targetExpenseID + updatedExpenses[targetExpenseID] = struct{}{} handledUpdate = true } else { requiresDelete = true @@ -426,6 +440,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ }).Error; err != nil { return err } + if link.ExpenseID != 0 { + updatedExpenses[link.ExpenseID] = struct{}{} + } handledUpdate = true } } @@ -464,6 +481,9 @@ 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. @@ -541,6 +561,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil { return err } + if expenseDetail != nil && expenseDetail.Id != 0 { + updatedExpenses[uint64(expenseDetail.Id)] = struct{}{} + } } // 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 } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 55e45a80..6efcf7ec 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -996,10 +996,8 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") } - itemIDs := make([]uint, 0, len(purchase.Items)) itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items)) for i, item := range purchase.Items { - itemIDs = append(itemIDs, item.Id) itemsToDelete[i] = item }