mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
406 lines
11 KiB
Go
406 lines
11 KiB
Go
package repositories
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type PurchaseRepository interface {
|
|
repository.BaseRepository[entity.Purchase]
|
|
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
|
CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error
|
|
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate) error
|
|
UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error
|
|
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
|
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
|
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
|
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
|
|
SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
|
GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
|
|
GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
|
|
}
|
|
|
|
type PurchaseRepositoryImpl struct {
|
|
*repository.BaseRepositoryImpl[entity.Purchase]
|
|
}
|
|
|
|
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
|
return &PurchaseRepositoryImpl{
|
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
|
|
}
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
|
|
db := r.DB().WithContext(ctx)
|
|
|
|
//ambil dari base repository
|
|
if err := db.Create(purchase).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, item := range items {
|
|
item.PurchaseId = purchase.Id
|
|
}
|
|
|
|
if err := db.Create(&items).Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error {
|
|
if purchaseID == 0 {
|
|
return nil
|
|
}
|
|
|
|
query := `
|
|
WITH latest_pfk AS (
|
|
SELECT pfk.id, pfk.kandang_id
|
|
FROM project_flock_kandangs pfk
|
|
JOIN (
|
|
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
|
FROM approvals
|
|
WHERE approvable_type = 'PROJECT_FLOCKS'
|
|
ORDER BY approvable_id, action_at DESC
|
|
) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id
|
|
WHERE LOWER(latest_approval.step_name) = LOWER('Aktif')
|
|
)
|
|
UPDATE purchase_items pi
|
|
SET project_flock_kandang_id = lp.id
|
|
FROM warehouses w
|
|
JOIN latest_pfk lp ON lp.kandang_id = w.kandang_id
|
|
WHERE pi.purchase_id = ?
|
|
AND pi.project_flock_kandang_id IS NULL
|
|
AND pi.warehouse_id = w.id;
|
|
`
|
|
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
|
if len(projectFlockKandangIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var purchaseIDs []uint
|
|
query := `
|
|
SELECT pi.purchase_id
|
|
FROM purchase_items pi
|
|
WHERE pi.project_flock_kandang_id IN (?)
|
|
GROUP BY pi.purchase_id
|
|
HAVING COUNT(*) = COUNT(CASE WHEN pi.project_flock_kandang_id IN (?) THEN 1 END)
|
|
`
|
|
if err := tx.Raw(query, projectFlockKandangIDs, projectFlockKandangIDs).Scan(&purchaseIDs).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
if len(purchaseIDs) > 0 {
|
|
if err := tx.Model(&entity.Purchase{}).
|
|
Where("id IN (?) AND deleted_at IS NULL", purchaseIDs).
|
|
Update("deleted_at", now).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("purchase_id IN (?)", purchaseIDs).Delete(&entity.PurchaseItem{}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
deleteItems := tx.Where("project_flock_kandang_id IN (?)", projectFlockKandangIDs)
|
|
if len(purchaseIDs) > 0 {
|
|
deleteItems = deleteItems.Where("purchase_id NOT IN (?)", purchaseIDs)
|
|
}
|
|
return deleteItems.Delete(&entity.PurchaseItem{}).Error
|
|
})
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, item := range items {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
item.PurchaseId = purchaseID
|
|
}
|
|
|
|
return r.DB().WithContext(ctx).Create(&items).Error
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) purchaseItemExists(ctx context.Context, purchaseID uint, itemID uint) (bool, error) {
|
|
var count int64
|
|
if err := r.DB().WithContext(ctx).
|
|
Model(&entity.PurchaseItem{}).
|
|
Where("purchase_id = ? AND id = ?", purchaseID, itemID).
|
|
Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
type PurchasePricingUpdate struct {
|
|
ItemID uint
|
|
ProductID *uint
|
|
Price float64
|
|
TotalPrice float64
|
|
Quantity *float64
|
|
TotalQty *float64
|
|
}
|
|
|
|
type PurchaseReceivingUpdate struct {
|
|
ItemID uint
|
|
ReceivedDate *time.Time
|
|
TravelNumber *string
|
|
TravelDocumentPath *string
|
|
VehicleNumber *string
|
|
ReceivedQty *float64
|
|
WarehouseID *uint
|
|
ProductWarehouseID *uint
|
|
ClearProductWarehouse bool
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) UpdatePricing(
|
|
ctx context.Context,
|
|
purchaseID uint,
|
|
updates []PurchasePricingUpdate,
|
|
) error {
|
|
if len(updates) == 0 {
|
|
return errors.New("pricing updates cannot be empty")
|
|
}
|
|
|
|
db := r.DB().WithContext(ctx)
|
|
|
|
for _, upd := range updates {
|
|
data := map[string]interface{}{
|
|
"price": upd.Price,
|
|
"total_price": upd.TotalPrice,
|
|
}
|
|
if upd.ProductID != nil {
|
|
data["product_id"] = *upd.ProductID
|
|
}
|
|
if upd.Quantity != nil {
|
|
data["sub_qty"] = *upd.Quantity
|
|
}
|
|
if upd.TotalQty != nil {
|
|
data["total_qty"] = *upd.TotalQty
|
|
}
|
|
|
|
result := db.Model(&entity.PurchaseItem{}).
|
|
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
|
Updates(data)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected == 0 {
|
|
exists, err := r.purchaseItemExists(ctx, purchaseID, upd.ItemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
|
ctx context.Context,
|
|
purchaseID uint,
|
|
updates []PurchaseReceivingUpdate,
|
|
) error {
|
|
if len(updates) == 0 {
|
|
return errors.New("receiving updates cannot be empty")
|
|
}
|
|
|
|
db := r.DB().WithContext(ctx)
|
|
|
|
for _, upd := range updates {
|
|
data := map[string]interface{}{}
|
|
|
|
if upd.ReceivedDate != nil {
|
|
data["received_date"] = upd.ReceivedDate
|
|
}
|
|
if upd.TravelNumber != nil {
|
|
data["travel_number"] = upd.TravelNumber
|
|
}
|
|
if upd.TravelDocumentPath != nil {
|
|
data["travel_number_docs"] = upd.TravelDocumentPath
|
|
}
|
|
if upd.VehicleNumber != nil {
|
|
data["vehicle_number"] = upd.VehicleNumber
|
|
}
|
|
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
|
|
data["warehouse_id"] = upd.WarehouseID
|
|
}
|
|
|
|
if upd.ProductWarehouseID != nil {
|
|
data["product_warehouse_id"] = *upd.ProductWarehouseID
|
|
} else if upd.ClearProductWarehouse {
|
|
data["product_warehouse_id"] = gorm.Expr("NULL")
|
|
}
|
|
|
|
if len(data) == 0 {
|
|
continue
|
|
}
|
|
|
|
result := db.Model(&entity.PurchaseItem{}).
|
|
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
|
Updates(data)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected == 0 {
|
|
exists, err := r.purchaseItemExists(ctx, purchaseID, upd.ItemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error {
|
|
if len(itemIDs) == 0 {
|
|
return errors.New("itemIDs cannot be empty")
|
|
}
|
|
|
|
return r.DB().WithContext(ctx).
|
|
Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
|
|
Delete(&entity.PurchaseItem{}).Error
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
|
return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
|
return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
|
|
db := tx
|
|
if db == nil {
|
|
db = r.DB()
|
|
}
|
|
|
|
var values []string
|
|
err := db.WithContext(ctx).
|
|
Model(&entity.Purchase{}).
|
|
Where(fmt.Sprintf("%s ILIKE ?", column), prefix+"%").
|
|
Select(column).
|
|
Order(fmt.Sprintf("%s DESC", column)).
|
|
Limit(20).
|
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Pluck(column, &values).Error
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
next := 1
|
|
for _, value := range values {
|
|
if number, ok := parseNumericSuffix(value, prefix); ok {
|
|
next = number + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
const maxAttempts = 20
|
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
|
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
|
|
exists, err := r.numberExists(ctx, db, column, candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !exists {
|
|
return candidate, nil
|
|
}
|
|
next++
|
|
}
|
|
|
|
return "", fmt.Errorf("unable to generate unique %s", column)
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
|
|
var count int64
|
|
if err := db.WithContext(ctx).
|
|
Model(&entity.Purchase{}).
|
|
Where(fmt.Sprintf("%s = ?", column), value).
|
|
Count(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) {
|
|
|
|
return r.GetItemsByWarehouseKandang(ctx, projectFlockID)
|
|
}
|
|
|
|
func (r *PurchaseRepositoryImpl) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) {
|
|
var items []entity.PurchaseItem
|
|
|
|
var kandangIDs []uint
|
|
err := r.DB().WithContext(ctx).
|
|
Table("project_flock_kandangs").
|
|
Where("project_flock_id = ?", projectFlockID).
|
|
Pluck("kandang_id", &kandangIDs).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(kandangIDs) == 0 {
|
|
return []entity.PurchaseItem{}, nil
|
|
}
|
|
|
|
err = r.DB().WithContext(ctx).
|
|
Preload("Product").
|
|
Preload("Product.Flags").
|
|
Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id").
|
|
Where("warehouses.kandang_id IN ?", kandangIDs).
|
|
Find(&items).Error
|
|
|
|
return items, err
|
|
}
|
|
|
|
func parseNumericSuffix(value, prefix string) (int, bool) {
|
|
if !strings.HasPrefix(value, prefix) {
|
|
return 0, false
|
|
}
|
|
suffix := strings.TrimPrefix(value, prefix)
|
|
if suffix == "" {
|
|
return 0, false
|
|
}
|
|
trimmed := strings.TrimLeft(suffix, "0")
|
|
if trimmed == "" {
|
|
trimmed = "0"
|
|
}
|
|
number, err := strconv.Atoi(trimmed)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return number, true
|
|
}
|