Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into Feat/BE/Expense_adjust_approval_flow

This commit is contained in:
aguhh18
2026-01-11 21:19:35 +07:00
32 changed files with 507 additions and 205 deletions
@@ -1 +1,2 @@
DROP TABLE IF EXISTS expenses; DROP SEQUENCE IF EXISTS expenses_ref_seq;
DROP TABLE IF EXISTS expenses;
@@ -1,3 +1,3 @@
-- Drop function and sequence for sales order numbers -- Drop function and sequence for sales order numbers
DROP FUNCTION IF EXISTS generate_so_number();
DROP SEQUENCE IF EXISTS so_number_seq; DROP SEQUENCE IF EXISTS so_number_seq;
DROP FUNCTION IF EXISTS generate_so_number();
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
DROP COLUMN IF EXISTS party_account_number;
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50);
COMMIT;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1,20 @@
-- Revert master data foreign keys to CASCADE delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,20 @@
-- Update master data foreign keys to RESTRICT delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,59 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mass'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh;
END IF;
END $$;
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3),
ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,3);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
(hand_day IS NULL OR hand_day >= 0) AND
(hand_house IS NULL OR hand_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mesh IS NULL OR egg_mesh >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
COMMIT;
@@ -0,0 +1,57 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
ALTER TABLE recordings
DROP COLUMN IF EXISTS daily_gain,
DROP COLUMN IF EXISTS avg_daily_gain;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mesh'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
(hen_day IS NULL OR hen_day >= 0) AND
(hen_house IS NULL OR hen_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mass IS NULL OR egg_mass >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
COMMIT;
+1
View File
@@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Tax: tax, Tax: tax,
ExpiryPeriod: seed.Expiry, ExpiryPeriod: seed.Expiry,
CreatedBy: createdBy, CreatedBy: createdBy,
IsVisible: seed.IsVisible,
} }
if err := tx.Create(&product).Error; err != nil { if err := tx.Create(&product).Error; err != nil {
return err return err
+17 -16
View File
@@ -7,22 +7,23 @@ import (
) )
type Payment struct { type Payment struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PaymentCode string `gorm:"type:varchar(50);not null"` PaymentCode string `gorm:"type:varchar(50);not null"`
ReferenceNumber *string `gorm:"type:varchar(100)"` ReferenceNumber *string `gorm:"type:varchar(100)"`
TransactionType string `gorm:"type:varchar(50)"` TransactionType string `gorm:"type:varchar(50)"`
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"` PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"` PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
PaymentDate time.Time `gorm:"not null"` PartyAccountNumber *string `gorm:"type:varchar(50)"`
PaymentMethod string `gorm:"type:varchar(20);not null"` PaymentDate time.Time `gorm:"not null"`
BankId *uint `gorm:"not null;index:idx_payments_bank_id"` PaymentMethod string `gorm:"type:varchar(20);not null"`
Direction string `gorm:"type:varchar(5);not null"` BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
Nominal float64 `gorm:"type:numeric(15,3);not null"` Direction string `gorm:"type:varchar(5);not null"`
Notes string `gorm:"type:text;not null"` Nominal float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` Notes string `gorm:"type:text;not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedBy uint `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"index" json:"-"`
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"` BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
+1 -1
View File
@@ -21,7 +21,7 @@ type Product struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
IsVisible bool `gorm:"column:is_visible;default:true"` IsVisible bool ``
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
+6 -6
View File
@@ -16,10 +16,10 @@ type Recording struct {
CumIntake *int `gorm:"column:cum_intake"` CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"` FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"` TotalChickQty *float64 `gorm:"column:total_chick_qty"`
HandDay *float64 `gorm:"column:hand_day"` HenDay *float64 `gorm:"column:hen_day"`
HandHouse *float64 `gorm:"column:hand_house"` HenHouse *float64 `gorm:"column:hen_house"`
FeedIntake *float64 `gorm:"column:feed_intake"` FeedIntake *float64 `gorm:"column:feed_intake"`
EggMesh *float64 `gorm:"column:egg_mesh"` EggMass *float64 `gorm:"column:egg_mass"`
EggWeight *float64 `gorm:"column:egg_weight"` EggWeight *float64 `gorm:"column:egg_weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -34,11 +34,11 @@ type Recording struct {
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
StandardHandDay *float64 `gorm:"-"` StandardHenDay *float64 `gorm:"-"`
StandardHandHouse *float64 `gorm:"-"` StandardHenHouse *float64 `gorm:"-"`
StandardFeedIntake *float64 `gorm:"-"` StandardFeedIntake *float64 `gorm:"-"`
StandardMaxDepletion *float64 `gorm:"-"` StandardMaxDepletion *float64 `gorm:"-"`
StandardEggMesh *float64 `gorm:"-"` StandardEggMass *float64 `gorm:"-"`
StandardEggWeight *float64 `gorm:"-"` StandardEggWeight *float64 `gorm:"-"`
StandardFcr *float64 `gorm:"-"` StandardFcr *float64 `gorm:"-"`
} }
@@ -101,20 +101,25 @@ func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO {
func partyFromInitial(e entity.Payment) Party { func partyFromInitial(e entity.Payment) Party {
party := Party{ party := Party{
Id: e.PartyId, Id: e.PartyId,
Type: e.PartyType, Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
} }
switch utils.PaymentParty(e.PartyType) { switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer: case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 { if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
} }
case utils.PaymentPartySupplier: case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil { if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber party.AccountNumber = *e.Supplier.AccountNumber
} }
} }
@@ -120,6 +120,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
TransactionType: string(utils.TransactionTypeSaldoAwal), TransactionType: string(utils.TransactionTypeSaldoAwal),
PartyType: party, PartyType: party,
PartyId: req.PartyId, PartyId: req.PartyId,
PartyAccountNumber: nil,
PaymentDate: time.Now(), PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo), PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId, BankId: req.BankId,
@@ -106,6 +106,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
TransactionType: string(utils.TransactionTypeInjection), TransactionType: string(utils.TransactionTypeInjection),
PartyType: string(utils.PaymentPartyCustomer), PartyType: string(utils.PaymentPartyCustomer),
PartyId: 0, PartyId: 0,
PartyAccountNumber: nil,
PaymentDate: adjustmentDate, PaymentDate: adjustmentDate,
PaymentMethod: string(utils.PaymentMethodSaldo), PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId, BankId: req.BankId,
@@ -1,17 +1,17 @@
package validation package validation
type Create struct { type Create struct {
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"` BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"` AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"` Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"` Notes string `json:"notes" validate:"required_strict,max=500"`
} }
type Update struct { type Update struct {
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"` AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type Query struct { type Query struct {
@@ -124,20 +124,25 @@ func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO {
func partyFromPayment(e entity.Payment) Party { func partyFromPayment(e entity.Payment) Party {
party := Party{ party := Party{
Id: e.PartyId, Id: e.PartyId,
Type: e.PartyType, Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
} }
switch utils.PaymentParty(e.PartyType) { switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer: case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 { if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
} }
case utils.PaymentPartySupplier: case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil { if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber party.AccountNumber = *e.Supplier.AccountNumber
} }
} }
+3 -3
View File
@@ -15,7 +15,7 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService
route := v1.Group("/payments") route := v1.Group("/payments")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne) route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
} }
@@ -121,18 +121,19 @@ func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
createBody := &entity.Payment{ createBody := &entity.Payment{
PaymentCode: code, PaymentCode: code,
ReferenceNumber: req.ReferenceNumber, ReferenceNumber: req.ReferenceNumber,
TransactionType: transactionType, TransactionType: transactionType,
PartyType: party, PartyType: party,
PartyId: req.PartyId, PartyId: req.PartyId,
PaymentDate: paymentDate, PartyAccountNumber: req.PartyAccountNumber,
PaymentMethod: method, PaymentDate: paymentDate,
BankId: req.BankId, PaymentMethod: method,
Direction: directionForParty(party), BankId: req.BankId,
Nominal: req.Nominal, Direction: directionForParty(party),
Notes: req.Notes, Nominal: req.Nominal,
CreatedBy: actorID, Notes: req.Notes,
CreatedBy: actorID,
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -188,6 +189,9 @@ func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if req.ReferenceNumber != nil { if req.ReferenceNumber != nil {
updateBody["reference_number"] = *req.ReferenceNumber updateBody["reference_number"] = *req.ReferenceNumber
} }
if req.PartyAccountNumber != nil {
updateBody["party_account_number"] = *req.PartyAccountNumber
}
if req.PaymentMethod != nil { if req.PaymentMethod != nil {
method, err := normalizePaymentMethod(*req.PaymentMethod) method, err := normalizePaymentMethod(*req.PaymentMethod)
if err != nil { if err != nil {
@@ -1,25 +1,27 @@
package validation package validation
type Create struct { type Create struct {
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"` PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"` PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"` PartyAccountNumber *string `json:"party_account_number"`
Nominal float64 `json:"nominal" validate:"required_strict"` PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
ReferenceNumber *string `json:"reference_number,omitempty"` Nominal float64 `json:"nominal" validate:"required_strict"`
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"` ReferenceNumber *string `json:"reference_number,omitempty"`
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"` PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
Notes string `json:"notes" validate:"required_strict,max=500"` BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
} }
type Update struct { type Update struct {
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"` PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"` PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"` PartyAccountNumber *string `json:"party_account_number,omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
ReferenceNumber *string `json:"reference_number,omitempty"` Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"` ReferenceNumber *string `json:"reference_number,omitempty"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type Query struct { type Query struct {
@@ -124,20 +124,25 @@ func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO {
func partyFromPayment(e entity.Payment) Party { func partyFromPayment(e entity.Payment) Party {
party := Party{ party := Party{
Id: e.PartyId, Id: e.PartyId,
Type: e.PartyType, Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
} }
switch utils.PaymentParty(e.PartyType) { switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer: case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 { if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
} }
case utils.PaymentPartySupplier: case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil { if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber party.AccountNumber = *e.Supplier.AccountNumber
} }
} }
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
@@ -380,3 +381,38 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Conte
} }
return &product, nil return &product, nil
} }
func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) {
if len(prefixes) == 0 {
return []uint{}, nil
}
db := r.DB().WithContext(ctx).
Model(&entity.Product{}).
Distinct("products.id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", entity.FlagableTypeProduct)
applied := false
for _, prefix := range prefixes {
if prefix == "" {
continue
}
like := prefix + "%"
if !applied {
db = db.Where("flags.name LIKE ?", like)
applied = true
continue
}
db = db.Or("flags.name LIKE ?", like)
}
if visibleStatus != nil {
db = db.Where("products.is_visible = ?", *visibleStatus)
}
var ids []uint
if err := db.Pluck("products.id", &ids).Error; err != nil {
return nil, err
}
return ids, nil
}
@@ -5,6 +5,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -19,6 +20,7 @@ type ProductRelationDTO struct {
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"` Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
} }
type ProductListDTO struct { type ProductListDTO struct {
@@ -33,6 +35,7 @@ type ProductListDTO struct {
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -70,6 +73,7 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
Flags: &flags, Flags: &flags,
Uom: uomRef, Uom: uomRef,
ProductCategory: categoryRef, ProductCategory: categoryRef,
Suppliers: toProductSupplierDTOs(e.ProductSuppliers),
} }
} }
@@ -112,6 +116,7 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
ProductCategory: categoryRef, ProductCategory: categoryRef,
Suppliers: toProductSupplierDTOs(e.ProductSuppliers),
} }
} }
@@ -128,3 +133,23 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
ProductListDTO: ToProductListDTO(e), ProductListDTO: ToProductListDTO(e),
} }
} }
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
}
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
}
if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
}
return result
}
@@ -23,6 +23,7 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories" uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -308,12 +309,12 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
} }
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
ProductionStandardId: req.ProductionStandardId, ProductionStandardId: req.ProductionStandardId,
LocationId: req.LocationId, LocationId: req.LocationId,
CreatedBy: actorID, CreatedBy: actorID,
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -823,22 +824,7 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs) // NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist.
if err != nil {
s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
}
if len(blocked) > 0 {
names := make([]string, 0, len(blocked))
for _, item := range blocked {
label := fmt.Sprintf("ID %d", item.Id)
if strings.TrimSpace(item.Name) != "" {
label = fmt.Sprintf("%s (%s)", label, item.Name)
}
names = append(names, label)
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
}
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs) pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
if err != nil { if err != nil {
@@ -854,6 +840,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang") return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
} }
db := s.Repository.DB()
if dbTransaction != nil {
db = dbTransaction
}
purchaseRepo := purchaseRepository.NewPurchaseRepository(db)
if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang")
}
pwRepo := s.ProductWarehouseRepo pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil { if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -906,6 +900,11 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
return nil return nil
} }
projectFlockID := records[0].ProjectFlockId
if projectFlockID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan")
}
pwRepo := s.ProductWarehouseRepo pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil { if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -920,24 +919,34 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB()) warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
} }
flags := []utils.FlagType{ db := s.Repository.DB()
utils.FlagAyamAfkir, if dbTransaction != nil {
utils.FlagAyamCulling, db = dbTransaction
utils.FlagAyamMati, }
utils.FlagTelurPecah, var category string
utils.FlagTelurUtuh, if err := db.WithContext(ctx).
Model(&entity.ProjectFlock{}).
Select("category").
Where("id = ?", projectFlockID).
Scan(&category).Error; err != nil {
return err
}
if strings.TrimSpace(category) == "" {
return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan")
} }
productIDs := make(map[utils.FlagType]uint, len(flags)) prefixes := []string{"AYAM-"}
for _, flag := range flags { if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag)) prefixes = append(prefixes, "TELUR")
if err != nil { }
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag)) invisibleOnly := false
} productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly)
return err if err != nil {
} return err
productIDs[flag] = product.Id }
if len(productIDs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", ")))
} }
for _, record := range records { for _, record := range records {
@@ -953,8 +962,7 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
return err return err
} }
for _, flag := range flags { for _, productID := range productIDs {
productID := productIDs[flag]
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil { if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
continue continue
} else if !errors.Is(err, gorm.ErrRecordNotFound) { } else if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -25,16 +25,16 @@ type RecordingRelationDTO struct {
CumIntake int `json:"cum_intake"` CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"` FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"` TotalChickQty float64 `json:"total_chick_qty"`
HandDay float64 `json:"hand_day"` HenDay float64 `json:"hen_day"`
HandHouse float64 `json:"hand_house"` HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"` FeedIntake float64 `json:"feed_intake"`
EggMesh float64 `json:"egg_mesh"` EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"` EggWeight float64 `json:"egg_weight"`
StandardHandDay *float64 `json:"hand_day_std,omitempty"` StandardHenDay *float64 `json:"hen_day_std,omitempty"`
StandardHandHouse *float64 `json:"hand_house_std,omitempty"` StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"` StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"` StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"` StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"` StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"` StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"` Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
@@ -94,10 +94,10 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
cumIntake int cumIntake int
fcrValue float64 fcrValue float64
totalChickQty float64 totalChickQty float64
handDay float64 henDay float64
handHouse float64 henHouse float64
feedIntake float64 feedIntake float64
eggMesh float64 eggMass float64
eggWeight float64 eggWeight float64
) )
@@ -119,17 +119,17 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
if e.TotalChickQty != nil { if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty totalChickQty = *e.TotalChickQty
} }
if e.HandDay != nil { if e.HenDay != nil {
handDay = *e.HandDay henDay = *e.HenDay
} }
if e.HandHouse != nil { if e.HenHouse != nil {
handHouse = *e.HandHouse henHouse = *e.HenHouse
} }
if e.FeedIntake != nil { if e.FeedIntake != nil {
feedIntake = *e.FeedIntake feedIntake = *e.FeedIntake
} }
if e.EggMesh != nil { if e.EggMass != nil {
eggMesh = *e.EggMesh eggMass = *e.EggMass
} }
if e.EggWeight != nil { if e.EggWeight != nil {
eggWeight = *e.EggWeight eggWeight = *e.EggWeight
@@ -157,16 +157,16 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
CumIntake: cumIntake, CumIntake: cumIntake,
FcrValue: fcrValue, FcrValue: fcrValue,
TotalChickQty: totalChickQty, TotalChickQty: totalChickQty,
HandDay: handDay, HenDay: henDay,
HandHouse: handHouse, HenHouse: henHouse,
FeedIntake: feedIntake, FeedIntake: feedIntake,
EggMesh: eggMesh, EggMass: eggMass,
EggWeight: eggWeight, EggWeight: eggWeight,
StandardHandDay: e.StandardHandDay, StandardHenDay: e.StandardHenDay,
StandardHandHouse: e.StandardHandHouse, StandardHenHouse: e.StandardHenHouse,
StandardFeedIntake: e.StandardFeedIntake, StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion, StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMesh: e.StandardEggMesh, StandardEggMass: e.StandardEggMass,
StandardEggWeight: e.StandardEggWeight, StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr, StandardFcr: e.StandardFcr,
Approval: latestApproval, Approval: latestApproval,
@@ -16,10 +16,10 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll) route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll)
route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay)
route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne)
route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne) route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne)
route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne) route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne)
route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne) route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne)
route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay)
route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve) route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve)
} }
@@ -901,47 +901,6 @@ type eggTotals struct {
Weight float64 Weight float64
} }
type stockTotals struct {
Usage float64
Pending float64
Total float64
}
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += usage
current.Pending += pending
current.Total += usage + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += stock.Qty
current.Pending += pending
current.Total += stock.Qty + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false hasPending := false
@@ -1156,34 +1115,34 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
recording.FeedIntake = nil recording.FeedIntake = nil
} }
var handDay float64 var henDay float64
if remainingChick > 0 && totalEggQty >= 0 { if remainingChick > 0 && totalEggQty >= 0 {
handDay = (totalEggQty / remainingChick) * 100 henDay = (totalEggQty / remainingChick) * 100
updates["hand_day"] = handDay updates["hen_day"] = henDay
recording.HandDay = &handDay recording.HenDay = &henDay
} else { } else {
updates["hand_day"] = gorm.Expr("NULL") updates["hen_day"] = gorm.Expr("NULL")
recording.HandDay = nil recording.HenDay = nil
} }
var handHouse float64 var henHouse float64
if initialChickin > 0 && cumulativeEggQty >= 0 { if initialChickin > 0 && cumulativeEggQty >= 0 {
handHouse = cumulativeEggQty / initialChickin henHouse = cumulativeEggQty / initialChickin
updates["hand_house"] = handHouse updates["hen_house"] = henHouse
recording.HandHouse = &handHouse recording.HenHouse = &henHouse
} else { } else {
updates["hand_house"] = gorm.Expr("NULL") updates["hen_house"] = gorm.Expr("NULL")
recording.HandHouse = nil recording.HenHouse = nil
} }
var eggMesh float64 var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 { if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMesh = (totalEggWeightGrams / remainingChick) * 1000 eggMass = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mesh"] = eggMesh updates["egg_mass"] = eggMass
recording.EggMesh = &eggMesh recording.EggMass = &eggMass
} else { } else {
updates["egg_mesh"] = gorm.Expr("NULL") updates["egg_mass"] = gorm.Expr("NULL")
recording.EggMesh = nil recording.EggMass = nil
} }
var eggWeight float64 var eggWeight float64
@@ -1334,11 +1293,11 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit
} }
type productionStandardValues struct { type productionStandardValues struct {
HandDay *float64 HenDay *float64
HandHouse *float64 HenHouse *float64
FeedIntake *float64 FeedIntake *float64
MaxDepletion *float64 MaxDepletion *float64
EggMesh *float64 EggMass *float64
EggWeight *float64 EggWeight *float64
} }
@@ -1389,10 +1348,10 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
return err return err
} }
if detail != nil { if detail != nil {
standard.HandDay = detail.TargetHenDayProduction standard.HenDay = detail.TargetHenDayProduction
standard.HandHouse = detail.TargetHenHouseProduction standard.HenHouse = detail.TargetHenHouseProduction
standard.EggWeight = detail.TargetEggWeight standard.EggWeight = detail.TargetEggWeight
standard.EggMesh = detail.TargetEggMass standard.EggMass = detail.TargetEggMass
} }
} }
@@ -1420,11 +1379,11 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
} }
} }
item.StandardHandDay = standard.HandDay item.StandardHenDay = standard.HenDay
item.StandardHandHouse = standard.HandHouse item.StandardHenHouse = standard.HenHouse
item.StandardFeedIntake = standard.FeedIntake item.StandardFeedIntake = standard.FeedIntake
item.StandardMaxDepletion = standard.MaxDepletion item.StandardMaxDepletion = standard.MaxDepletion
item.StandardEggMesh = standard.EggMesh item.StandardEggMass = standard.EggMass
item.StandardEggWeight = standard.EggWeight item.StandardEggWeight = standard.EggWeight
item.StandardFcr = standardFcr item.StandardFcr = standardFcr
@@ -25,6 +25,7 @@ type PurchaseRepository interface {
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
} }
@@ -89,6 +90,44 @@ WHERE pi.purchase_id = ?
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error 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 { func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
if len(items) == 0 { if len(items) == 0 {
return nil return nil
@@ -133,6 +133,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db = db.Where("purchases.deleted_at IS NULL")
if params.SupplierID > 0 { if params.SupplierID > 0 {
db = db.Where("supplier_id = ?", params.SupplierID) db = db.Where("supplier_id = ?", params.SupplierID)
+38
View File
@@ -2,11 +2,14 @@ package utils
import ( import (
"errors" "errors"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/validation" "gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/jackc/pgconn"
pgconnv5 "github.com/jackc/pgx/v5/pgconn"
) )
func ErrorHandler(c *fiber.Ctx, err error) error { func ErrorHandler(c *fiber.Ctx, err error) error {
@@ -14,6 +17,10 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
return response.Error(c, fiber.StatusBadRequest, message, nil) return response.Error(c, fiber.StatusBadRequest, message, nil)
} }
if statusCode, message := mapPgError(err); statusCode != 0 {
return response.Error(c, statusCode, message, nil)
}
var fiberErr *fiber.Error var fiberErr *fiber.Error
if errors.As(err, &fiberErr) { if errors.As(err, &fiberErr) {
return response.Error(c, fiberErr.Code, fiberErr.Message, nil) return response.Error(c, fiberErr.Code, fiberErr.Message, nil)
@@ -26,6 +33,37 @@ func NotFoundHandler(c *fiber.Ctx) error {
return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil) return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil)
} }
func mapPgError(err error) (int, string) {
code, message := getPgErrorDetails(err)
if code == "" {
return 0, ""
}
switch code {
case "23503":
return fiber.StatusConflict, "Data tidak bisa dihapus karena masih digunakan oleh data lain."
case "P0001":
if strings.HasPrefix(message, "Cannot soft delete") {
return fiber.StatusConflict, "Data tidak bisa dihapus karena masih digunakan oleh data lain."
}
}
return 0, ""
}
func getPgErrorDetails(err error) (string, string) {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code, pgErr.Message
}
var pgErrV5 *pgconnv5.PgError
if errors.As(err, &pgErrV5) {
return pgErrV5.Code, pgErrV5.Message
}
return "", ""
}
func BadRequest(msg string) error { func BadRequest(msg string) error {
return fiber.NewError(fiber.StatusBadRequest, msg) return fiber.NewError(fiber.StatusBadRequest, msg)