fix purchase due date

This commit is contained in:
ragilap
2025-12-10 21:10:46 +07:00
parent 086184bbaa
commit de6304332b
7 changed files with 104 additions and 81 deletions
@@ -0,0 +1,2 @@
ALTER TABLE purchases
DROP COLUMN IF EXISTS credit_term;
@@ -0,0 +1,5 @@
ALTER TABLE purchases
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
ALTER TABLE purchases
ALTER COLUMN credit_term DROP DEFAULT;
+3 -2
View File
@@ -5,17 +5,18 @@ import (
) )
type Purchase struct { type Purchase struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PrNumber string `gorm:"not null"` PrNumber string `gorm:"not null"`
PoNumber *string PoNumber *string
PoDate *time.Time PoDate *time.Time
SupplierId uint `gorm:"not null"` SupplierId uint `gorm:"not null"`
CreditTerm int `gorm:"column:credit_term;not null;default:0"`
DueDate *time.Time DueDate *time.Time
Notes *string Notes *string
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt *time.Time `gorm:"index"` DeletedAt *time.Time `gorm:"index"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
// Relations // Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
+58 -57
View File
@@ -4,7 +4,7 @@ import (
"strings" "strings"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"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"
@@ -31,65 +31,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()
} }
} }
@@ -104,11 +104,12 @@ 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).
+12 -10
View File
@@ -14,11 +14,12 @@ import (
) )
type PurchaseRelationDTO struct { type PurchaseRelationDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
PrNumber string `json:"pr_number"` PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"` PoNumber *string `json:"po_number"`
PoDate *time.Time `json:"po_date"` PoDate *time.Time `json:"po_date"`
Notes *string `json:"notes"` CreditTerm int `json:"credit_term"`
Notes *string `json:"notes"`
} }
type PurchaseListDTO struct { type PurchaseListDTO struct {
@@ -64,11 +65,12 @@ type PurchaseItemDTO struct {
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO { func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
return PurchaseRelationDTO{ return PurchaseRelationDTO{
Id: p.Id, Id: p.Id,
PrNumber: p.PrNumber, PrNumber: p.PrNumber,
PoNumber: p.PoNumber, PoNumber: p.PoNumber,
PoDate: p.PoDate, PoDate: p.PoDate,
Notes: p.Notes, CreditTerm: p.CreditTerm,
Notes: p.Notes,
} }
} }
@@ -309,21 +309,17 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
indexMap[key] = len(aggregated) - 1 indexMap[key] = len(aggregated) - 1
} }
// var dueDate *time.Time var dueDate *time.Time
// if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" { now := time.Now().UTC()
// parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate)) d := now.AddDate(0, 0, req.CreditTerm)
// if err != nil { dueDate = &d
// return nil, utils.BadRequest("Invalid due_date, expected YYYY-MM-DD")
// }
// parsed = parsed.UTC()
// dueDate = &parsed
// }
purchase := &entity.Purchase{ purchase := &entity.Purchase{
SupplierId: uint(req.SupplierID), SupplierId: uint(req.SupplierID),
// DueDate: dueDate, CreditTerm: req.CreditTerm,
Notes: req.Notes, DueDate: dueDate,
CreatedBy: uint(actorID), Notes: req.Notes,
CreatedBy: uint(actorID),
} }
items := make([]*entity.PurchaseItem, 0, len(aggregated)) items := make([]*entity.PurchaseItem, 0, len(aggregated))
@@ -683,6 +679,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
visitedItems := make(map[uint]struct{}, len(req.Items)) visitedItems := make(map[uint]struct{}, len(req.Items))
prepared := make([]preparedReceiving, 0, len(req.Items)) prepared := make([]preparedReceiving, 0, len(req.Items))
var earliestReceived *time.Time
for _, payload := range req.Items { for _, payload := range req.Items {
item, exists := itemMap[payload.PurchaseItemID] item, exists := itemMap[payload.PurchaseItemID]
if !exists { if !exists {
@@ -694,6 +691,10 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return nil, utils.BadRequest(fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID)) return nil, utils.BadRequest(fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
} }
receivedDate = receivedDate.UTC() receivedDate = receivedDate.UTC()
if earliestReceived == nil || receivedDate.Before(*earliestReceived) {
copy := receivedDate
earliestReceived = &copy
}
warehouseID := uint(item.WarehouseId) warehouseID := uint(item.WarehouseId)
overrideWarehouse := false overrideWarehouse := false
@@ -869,6 +870,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err return err
} }
// Update due_date based on earliest received date when receiving approved.
if earliestReceived != nil {
due := earliestReceived.AddDate(0, 0, purchase.CreditTerm)
if err := tx.Model(&entity.Purchase{}).
Where("id = ?", purchase.Id).
Update("due_date", due).Error; err != nil {
return err
}
}
if s.FifoSvc != nil { if s.FifoSvc != nil {
for _, adj := range fifoAdds { for _, adj := range fifoAdds {
if adj.pwID == 0 || adj.qty <= 0 { if adj.pwID == 0 || adj.qty <= 0 {
@@ -8,6 +8,7 @@ type PurchaseItemPayload struct {
type CreatePurchaseRequest struct { type CreatePurchaseRequest struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"` SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
CreditTerm int `json:"credit_term" validate:"required,number,gte=0"`
DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"` DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Notes *string `json:"notes" validate:"omitempty,max=500"` Notes *string `json:"notes" validate:"omitempty,max=500"`
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"` Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`