diff --git a/internal/database/migrations/20251117034511_create_expenses_table.up.sql b/internal/database/migrations/20251117034511_create_expenses_table.up.sql index 8949d931..f5f20f2c 100644 --- a/internal/database/migrations/20251117034511_create_expenses_table.up.sql +++ b/internal/database/migrations/20251117034511_create_expenses_table.up.sql @@ -1,7 +1,7 @@ CREATE TABLE expenses ( id BIGSERIAL PRIMARY KEY, reference_number VARCHAR(50) UNIQUE NOT NULL, - supplier_id BIGINT NULL, + supplier_id BIGINT NOT NULL, category VARCHAR(50) NOT NULL CHECK ( category IN ('BOP', 'NON-BOP') ), diff --git a/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.down.sql b/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql b/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql new file mode 100644 index 00000000..ce71256b --- /dev/null +++ b/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql @@ -0,0 +1,44 @@ +-- ============================ +-- EXPENSES +-- ============================ +ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total; + +ALTER TABLE expenses RENAME COLUMN note TO notes; + +ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date; + +-- ============================ +-- EXPENSE_REALIZATIONS +-- ============================ +ALTER TABLE expense_realizations +RENAME COLUMN realization_qty TO qty; + +ALTER TABLE expense_realizations +RENAME COLUMN realization_unit_price TO price; + +ALTER TABLE expense_realizations RENAME COLUMN note TO notes; + +ALTER TABLE expense_realizations +DROP COLUMN IF EXISTS realization_total_price; + +ALTER TABLE expense_realizations +DROP COLUMN IF EXISTS realization_date; + +ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by; + +ALTER TABLE expense_realizations +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); + +-- ============================ +-- EXPENSE_NONSTOCKS +-- ============================ +ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes; + +ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price; + +ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price; + +ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by; + +ALTER TABLE expense_nonstocks +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); \ No newline at end of file diff --git a/internal/database/migrations/20251127070744_create_project_budgets.down.sql b/internal/database/migrations/20251127070744_create_project_budgets.down.sql new file mode 100644 index 00000000..55bfdb3d --- /dev/null +++ b/internal/database/migrations/20251127070744_create_project_budgets.down.sql @@ -0,0 +1,2 @@ +DROP Table IF EXISTS project_budgets; + diff --git a/internal/database/migrations/20251127070744_create_project_budgets.up.sql b/internal/database/migrations/20251127070744_create_project_budgets.up.sql new file mode 100644 index 00000000..db4a713b --- /dev/null +++ b/internal/database/migrations/20251127070744_create_project_budgets.up.sql @@ -0,0 +1,31 @@ +CREATE TABLE project_budgets ( + id BIGSERIAL PRIMARY KEY, + project_flock_id BIGINT NOT NULL, + nonstock_id BIGINT NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + price NUMERIC(15, 3) NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- Tambahkan Foreign Key ke project_flocks +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN + ALTER TABLE project_budgets + ADD CONSTRAINT fk_project_budgets_project_flock_id + FOREIGN KEY (project_flock_id) REFERENCES project_flocks(id); + END IF; +END $$; +-- Tambahkan Foreign Key ke nonstocks +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN + ALTER TABLE project_budgets + ADD CONSTRAINT fk_project_budgets_nonstock_id + FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id); + END IF; +END $$; +-- Index +CREATE INDEX idx_project_budgets_project_flock_id ON project_budgets (project_flock_id); + +CREATE INDEX idx_project_budgets_nonstock_id ON project_budgets (nonstock_id); \ No newline at end of file diff --git a/internal/entities/expense.go b/internal/entities/expense.go index 74998e6a..e6ab1d77 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -13,18 +13,16 @@ type Expense struct { SupplierId uint64 `gorm:""` Category string `gorm:"type:varchar(50);not null"` PoNumber string `gorm:"type:varchar(50)"` - DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan - RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi - RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi - ExpenseDate time.Time `gorm:"type:date;not null"` - GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` - Note string `gorm:"type:text"` + DocumentPath sql.NullString `gorm:"type:json"` + RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` + RealizationDate time.Time `gorm:"type:date;column:realization_date"` + TransactionDate time.Time `gorm:"type:date;not null"` + Notes string `gorm:"type:text;column:notes"` CreatedBy uint64 `gorm:""` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - // Relations Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go index 7be2053a..ccd4194c 100644 --- a/internal/entities/expense_nonstock.go +++ b/internal/entities/expense_nonstock.go @@ -1,20 +1,23 @@ package entities -type ExpenseNonstock struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseId *uint64 `gorm:""` - ProjectFlockKandangId *uint64 `gorm:""` - KandangId *uint64 `gorm:""` - NonstockId *uint64 `gorm:""` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` - Note string `gorm:"type:text"` +import ( + "time" +) + +type ExpenseNonstock struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseId *uint64 `gorm:""` + ProjectFlockKandangId *uint64 `gorm:""` + KandangId *uint64 `gorm:""` + NonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + Price float64 `gorm:"type:numeric(15,3);not null;column:price"` + Notes string `gorm:"type:text;column:notes"` + CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` - // Relations Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` - Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` + Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"` } diff --git a/internal/entities/expense_realization.go b/internal/entities/expense_realization.go index 629fdfb7..3c4b1f07 100644 --- a/internal/entities/expense_realization.go +++ b/internal/entities/expense_realization.go @@ -5,16 +5,12 @@ import ( ) type ExpenseRealization struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseNonstockId *uint64 `gorm:""` - RealizationQty float64 `gorm:"type:numeric(15,3);not null"` - RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` - RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` - RealizationDate time.Time `gorm:"type:date;not null"` - Note *string `gorm:"type:text"` - CreatedBy *uint64 `gorm:""` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseNonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null;"` + Price float64 `gorm:"type:numeric(15,3);not null;"` + Notes string `gorm:"type:text;"` + CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` - // Relations ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/project_budget.go b/internal/entities/project_budget.go new file mode 100644 index 00000000..23c521ac --- /dev/null +++ b/internal/entities/project_budget.go @@ -0,0 +1,15 @@ +package entities + +import ( + "time" +) + +type ProjectBudget struct { + Id uint `gorm:"primaryKey"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + Price float64 `gorm:"type:numeric(15,3);not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"` + ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"` +} diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 08256b24..25806ebb 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -96,30 +96,30 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { } req.Documents = form.File["documents"] - costPerKandangJSON := c.FormValue("cost_per_kandangs") - if costPerKandangJSON != "" { + expenseNonstocksJSON := c.FormValue("expense_nonstocks") + if expenseNonstocksJSON != "" { - if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil { + if err := json.Unmarshal([]byte(expenseNonstocksJSON), &req.ExpenseNonstocks); err != nil { - var singleCostPerKandang validation.CostPerKandang - if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err)) + var singleExpenseNonstock validation.ExpenseNonstock + if err := json.Unmarshal([]byte(expenseNonstocksJSON), &singleExpenseNonstock); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err)) } - if singleCostPerKandang.KandangID == 0 { + if singleExpenseNonstock.KandangID == 0 { return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required") } - req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang} + req.ExpenseNonstocks = []validation.ExpenseNonstock{singleExpenseNonstock} } else { - for i, costPerKandang := range req.CostPerKandangs { - if costPerKandang.KandangID == 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i)) + for i, expenseNonstock := range req.ExpenseNonstocks { + if expenseNonstock.KandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i)) } } } } else { - return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required") + return fiber.NewError(fiber.StatusBadRequest, "Field expense_nonstocks is required") } result, err := u.ExpenseService.CreateOne(c, req) @@ -155,20 +155,32 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error { req.TransactionDate = &transactionDate } - costPerKandangJSON := c.FormValue("cost_per_kandang") - if costPerKandangJSON != "" { - var costPerKandang []validation.CostPerKandang - if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err)) + categoryVal := c.FormValue("category") + req.Category = &categoryVal + + supplierIDVal := c.FormValue("supplier_id") + if supplierIDVal != "" { + supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format") + } + req.SupplierID = &supplierID + } + + expenseNonstocksJSON := c.FormValue("expense_nonstocks") + if expenseNonstocksJSON != "" { + var expenseNonstocks []validation.ExpenseNonstock + if err := json.Unmarshal([]byte(expenseNonstocksJSON), &expenseNonstocks); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err)) } - for i, costPerKandang := range costPerKandang { - if costPerKandang.KandangID == 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i)) + for i, expenseNonstock := range expenseNonstocks { + if expenseNonstock.KandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i)) } } - req.CostPerKandang = &costPerKandang + req.ExpenseNonstocks = &expenseNonstocks } result, err := u.ExpenseService.UpdateOne(c, req, uint(id)) diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index bee50c6d..c55dba2c 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -15,10 +15,9 @@ import ( // === DTO Structs === type ExpenseRelationDTO struct { - Id uint64 `json:"id"` - PoNumber string `json:"po_number"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + Id uint64 `json:"id"` + PoNumber string `json:"po_number"` + TransactionDate time.Time `json:"transaction_date"` } type ExpenseBaseDTO struct { @@ -28,8 +27,7 @@ type ExpenseBaseDTO struct { Category string `json:"category"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"` RealizationDate *time.Time `json:"realization_date,omitempty"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + TransactionDate time.Time `json:"transaction_date"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` } @@ -55,21 +53,26 @@ type ExpenseDetailDTO struct { } type ExpenseNonstockDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Note *string `json:"note,omitempty"` - Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + Id uint64 `json:"id"` + ExpenseId *uint64 `json:"expense_id,omitempty"` + ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"` + KandangId *uint64 `json:"kandang_id,omitempty"` + NonstockId *uint64 `json:"nonstock_id,omitempty"` + Qty float64 `json:"qty"` + Price float64 `json:"price"` + Notes string `json:"notes"` + Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + CreatedAt time.Time `json:"created_at"` } type ExpenseRealizationDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Note *string `json:"note,omitempty"` - Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + Id uint64 `json:"id"` + ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"` + Qty float64 `json:"qty"` + Price float64 `json:"price"` + Notes string `json:"notes"` + Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + CreatedAt time.Time `json:"created_at"` } type KandangGroupDTO struct { @@ -89,10 +92,9 @@ type DocumentDTO struct { func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO { return ExpenseRelationDTO{ - Id: e.Id, - PoNumber: e.PoNumber, - ExpenseDate: e.ExpenseDate, - GrandTotal: e.GrandTotal, + Id: e.Id, + PoNumber: e.PoNumber, + TransactionDate: e.TransactionDate, } } @@ -124,8 +126,7 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { Category: e.Category, Supplier: supplier, RealizationDate: realizationDate, - ExpenseDate: e.ExpenseDate, - GrandTotal: e.GrandTotal, + TransactionDate: e.TransactionDate, Location: location, } } @@ -192,10 +193,9 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { for _, ns := range e.Nonstocks { pengajuanDTO := ToExpenseNonstockDTO(ns) - pengajuans = append(pengajuans, pengajuanDTO) - if ns.Realization != nil && ns.Realization.Id != 0 { + if ns.Realization != nil { var nonstock *nonstockDTO.NonstockRelationDTO if ns.Nonstock != nil && ns.Nonstock.Id != 0 { mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock) @@ -203,12 +203,13 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { } realisasiDTO := ExpenseRealizationDTO{ - Id: ns.Realization.Id, - Qty: ns.Realization.RealizationQty, - UnitPrice: ns.Realization.RealizationUnitPrice, - TotalPrice: ns.Realization.RealizationTotalPrice, - Note: ns.Realization.Note, - Nonstock: nonstock, + Id: ns.Realization.Id, + ExpenseNonstockId: ns.Realization.ExpenseNonstockId, + Qty: ns.Realization.Qty, + Price: ns.Realization.Price, + Notes: ns.Realization.Notes, + Nonstock: nonstock, + CreatedAt: ns.Realization.CreatedAt, } realisasi = append(realisasi, realisasiDTO) } @@ -217,12 +218,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { var totalPengajuan float64 for _, p := range pengajuans { - totalPengajuan += p.TotalPrice + totalPengajuan += p.Qty * p.Price } var totalRealisasi float64 for _, r := range realisasi { - totalRealisasi += r.TotalPrice + totalRealisasi += r.Qty * r.Price } kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks) @@ -248,12 +249,16 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO { } return ExpenseNonstockDTO{ - Id: ns.Id, - Qty: ns.Qty, - UnitPrice: ns.UnitPrice, - TotalPrice: ns.TotalPrice, - Note: &ns.Note, - Nonstock: nonstock, + Id: ns.Id, + ExpenseId: ns.ExpenseId, + ProjectFlockKandangId: ns.ProjectFlockKandangId, + KandangId: ns.KandangId, + NonstockId: ns.NonstockId, + Qty: ns.Qty, + Price: ns.Price, + Notes: ns.Notes, + Nonstock: nonstock, + CreatedAt: ns.CreatedAt, } } @@ -264,11 +269,13 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali var kandangId uint64 var kandangName string - for _, ns := range nonstocks { - if ns.Id == p.Id && ns.Kandang != nil { - kandangId = uint64(ns.Kandang.Id) - kandangName = ns.Kandang.Name - break + if p.KandangId != nil { + kandangId = *p.KandangId + for _, ns := range nonstocks { + if ns.Id == p.Id && ns.Kandang != nil { + kandangName = ns.Kandang.Name + break + } } } diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go index 588583da..9e97a180 100644 --- a/internal/modules/expenses/repositories/expense.repository.go +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -10,7 +10,7 @@ import ( type ExpenseRepository interface { repository.BaseRepository[entity.Expense] - IdExists(ctx context.Context, id uint64) (bool, error) + IdExists(ctx context.Context, id uint) (bool, error) GetNextSequence(ctx context.Context) (int, error) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) } @@ -25,8 +25,8 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository { } } -func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { - return repository.Exists[entity.Expense](ctx, r.DB(), uint(id)) +func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Expense](ctx, r.DB(), id) } func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) { diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 0d0779f0..2bd00a0f 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "mime/multipart" - "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -148,8 +147,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return nil, err } - for _, costPerKandang := range req.CostPerKandangs { - for _, costItem := range costPerKandang.CostItems { + for _, expenseNonstock := range req.ExpenseNonstocks { + for _, costItem := range expenseNonstock.CostItems { nonstockId := uint(costItem.NonstockID) if err := commonSvc.EnsureRelations(c.Context(), @@ -189,21 +188,13 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number") } - var grandTotal float64 - for _, costPerKandang := range req.CostPerKandangs { - for _, costItem := range costPerKandang.CostItems { - grandTotal += costItem.TotalCost - } - } - createdBy := uint64(1) //todo get from auth expense = &entity.Expense{ ReferenceNumber: referenceNumber, PoNumber: req.PoNumber, Category: req.Category, SupplierId: req.SupplierID, - ExpenseDate: expenseDate, - GrandTotal: grandTotal, + TransactionDate: expenseDate, CreatedBy: createdBy, } @@ -211,15 +202,15 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") } - if len(req.CostPerKandangs) > 0 { + if len(req.ExpenseNonstocks) > 0 { - for _, costPerKandang := range req.CostPerKandangs { + for _, expenseNonstock := range req.ExpenseNonstocks { var projectFlockKandangId *uint64 if req.Category == "BOP" { - projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID)) + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") @@ -230,16 +221,16 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen projectFlockKandangId = &id } - for _, costItem := range costPerKandang.CostItems { + for _, costItem := range expenseNonstock.CostItems { nonstockId := costItem.NonstockID var kandangId *uint64 if req.Category == "NON-BOP" { - id := uint64(costPerKandang.KandangID) + id := uint64(expenseNonstock.KandangID) kandangId = &id } else if req.Category == "BOP" { if projectFlockKandangId != nil { - kandangId = &costPerKandang.KandangID + kandangId = &expenseNonstock.KandangID } } @@ -249,8 +240,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen KandangId: kandangId, NonstockId: &nonstockId, Qty: costItem.Quantity, - TotalPrice: costItem.TotalCost, - Note: costItem.Notes, + Price: costItem.Price, + Notes: costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { @@ -302,9 +293,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -328,10 +317,27 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") } - updateBody["expense_date"] = expenseDate + updateBody["transaction_date"] = expenseDate } - if len(updateBody) == 0 && req.CostPerKandang == nil && len(req.Documents) == 0 { + if req.Category != nil { + updateBody["category"] = *req.Category + } + + if req.SupplierID != nil { + supplierID := uint(*req.SupplierID) + supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) { + return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc}, + ); err != nil { + return nil, err + } + updateBody["supplier_id"] = *req.SupplierID + } + + if len(updateBody) == 0 && req.ExpenseNonstocks == nil && len(req.Documents) == 0 { responseDTO, err := s.GetOne(c, id) if err != nil { @@ -346,6 +352,21 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + currentExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense") + } + + categoryChanged := false + var newCategory string + if req.Category != nil && *req.Category != currentExpense.Category { + categoryChanged = true + newCategory = *req.Category + } + if len(updateBody) > 0 { if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -355,41 +376,79 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } } - if req.CostPerKandang != nil { + if categoryChanged { + if currentExpense.Category == "BOP" && newCategory == "NON-BOP" { - if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items") + var existingExpenseNonstocks []entity.ExpenseNonstock + if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks") + } + + for _, ens := range existingExpenseNonstocks { + updateData := map[string]interface{}{ + "project_flock_kandang_id": nil, + } + if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null") + } + } + } else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" { + + var existingExpenseNonstocks []entity.ExpenseNonstock + if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks") + } + + projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) + for _, ens := range existingExpenseNonstocks { + if ens.KandangId != nil { + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + } + projectFlockKandangId := uint64(projectFlockKandang.Id) + + updateData := map[string]interface{}{ + "project_flock_kandang_id": projectFlockKandangId, + } + if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id") + } + } + } + } + } + + if req.ExpenseNonstocks != nil { + + var existingExpenseNonstocks []entity.ExpenseNonstock + if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks for deletion") } - var grandTotal float64 - for _, cpk := range *req.CostPerKandang { - for _, costItem := range cpk.CostItems { - grandTotal += costItem.TotalCost + for _, ens := range existingExpenseNonstocks { + if err := expenseNonstockRepoTx.DeleteOne(c.Context(), uint(ens.Id)); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete expense nonstock") } } - if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{ - "grand_total": grandTotal, - }, nil); err != nil { - - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total") + updatedExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get updated expense") } - for _, cpk := range *req.CostPerKandang { + for _, expenseNonstock := range *req.ExpenseNonstocks { var projectFlockKandangId *uint64 - expense, err := expenseRepoTx.GetByID(c.Context(), id, nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Expense not found") - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense") - } - - if expense.Category == "BOP" { - + if updatedExpense.Category == "BOP" { projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) - projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID)) + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") @@ -400,7 +459,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) projectFlockKandangId = &id } - for _, costItem := range cpk.CostItems { + for _, costItem := range expenseNonstock.CostItems { nonstockId := uint(costItem.NonstockID) if err := commonSvc.EnsureRelations(c.Context(), @@ -410,13 +469,12 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } var kandangId *uint64 - if expense.Category == "NON-BOP" { - id := uint64(cpk.KandangID) + if updatedExpense.Category == "NON-BOP" { + id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if expense.Category == "BOP" { - + } else if updatedExpense.Category == "BOP" { if projectFlockKandangId != nil { - kandangId = &cpk.KandangID + kandangId = &expenseNonstock.KandangID } } @@ -427,8 +485,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) KandangId: kandangId, NonstockId: &costItem.NonstockID, Qty: costItem.Quantity, - TotalPrice: costItem.TotalCost, - Note: costItem.Notes, + Price: costItem.Price, + Notes: costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { @@ -481,9 +539,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return err } @@ -506,9 +562,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va } if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -518,8 +572,6 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") } - createdBy := uint64(1) // TODO: replace with authenticated user id - if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) @@ -543,13 +595,14 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va } realization := &entity.ExpenseRealization{ - ExpenseNonstockId: &expenseNonstockID, - RealizationQty: realizationItem.Qty, - RealizationUnitPrice: realizationItem.UnitPrice, - RealizationTotalPrice: realizationItem.TotalPrice, - RealizationDate: realizationDate, - Note: realizationItem.Notes, - CreatedBy: &createdBy, + ExpenseNonstockId: &expenseNonstockID, + Qty: realizationItem.Qty, + Price: realizationItem.Price, + Notes: "", + } + + if realizationItem.Notes != nil { + realization.Notes = *realizationItem.Notes } if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil { @@ -576,7 +629,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va expenseID, utils.ExpenseStepRealisasi, &approvalAction, - uint(createdBy), + uint(1), // TODO: replace with authenticated user id nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") @@ -597,9 +650,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) { if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -652,14 +703,12 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) ( func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { - s.Log.Errorf("Validation failed for UpdateRealization: %+v", err) + return nil, err } if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -669,66 +718,56 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") } - if latestApproval != nil && latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) { + if latestApproval != nil && (latestApproval.StepNumber < uint16(utils.ExpenseStepRealisasi)) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] return nil, fiber.NewError(fiber.StatusBadRequest, - fmt.Sprintf("Cannot update realization at %s step. Must be at Realisasi step", currentStepName)) + fmt.Sprintf("tidak bisa update realisasi pada step %s. Harus pada step Realisasi atau selesai", currentStepName)) } - var realizationDate *time.Time - if req.RealizationDate != "" { - parsedDate, err := utils.ParseDateString(req.RealizationDate) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") - } - realizationDate = &parsedDate - } - - if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) realizationRepoTx := repository.NewExpenseRealizationRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) expenseRepoTx := repository.NewExpenseRepository(tx) - for _, realizationItem := range req.Realizations { + // Check if only updating documents + updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0 - expenseNonstockID := realizationItem.ExpenseNonstockID + if len(req.Realizations) > 0 { + for _, realizationItem := range req.Realizations { - if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil { - return err - } + expenseNonstockID := realizationItem.ExpenseNonstockID - existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") + if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil { + return err } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") + existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + + return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") + } + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") + } + + updateData := map[string]interface{}{ + "qty": realizationItem.Qty, + "price": realizationItem.Price, + } + + if realizationItem.Notes != nil { + updateData["notes"] = *realizationItem.Notes + } + + if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil { + s.Log.Errorf("Failed to update realization: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization") + } } - updateData := map[string]interface{}{ - "realization_qty": realizationItem.Qty, - "realization_unit_price": realizationItem.UnitPrice, - "realization_total_price": realizationItem.TotalPrice, - "realization_date": *realizationDate, - } - - if realizationItem.Notes != nil { - updateData["note"] = *realizationItem.Notes - } - - if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil { - s.Log.Errorf("Failed to update realization: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization") - } - } - - if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{ - "realization_date": *realizationDate, - }, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date") } if len(req.Documents) > 0 { @@ -737,9 +776,28 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va } } + if !updateDataOnly && *latestApproval.Action == entity.ApprovalActionUpdated { + actorID := uint(1) // TODO: replace with authenticated user id + approvalAction := entity.ApprovalActionUpdated + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + expenseID, + utils.ExpenseStepRealisasi, + &approvalAction, + actorID, + nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") + } + } + return nil - }); err != nil { - return nil, err + }) + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "gagal update realisasi expense") } responseDTO, err := s.GetOne(c, expenseID) @@ -825,9 +883,7 @@ func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx reposito func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error { if err := commonSvc.EnsureRelations(ctx.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, ); err != nil { return err } @@ -909,9 +965,7 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, for _, id := range req.ApprovableIds { if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return err } diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 4e909b66..abe6198c 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -5,15 +5,15 @@ import ( ) type Create struct { - PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"` - TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"` - Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"` - SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"` - Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` - CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"` + PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"` + TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"` + Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"` + SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` + ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"` } -type CostPerKandang struct { +type ExpenseNonstock struct { KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"` CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"` } @@ -21,14 +21,16 @@ type CostPerKandang struct { type CostItem struct { NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"` Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"` - TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"` + Price float64 `form:"price" json:"price" validate:"required,gt=0"` Notes string `form:"notes" json:"notes" validate:"required,max=500"` } type Update struct { - TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"` - CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"` - Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` + TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"` + Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"` + SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"` + ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` } type Query struct { @@ -52,8 +54,7 @@ type UpdateRealization struct { type RealizationItem struct { ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"` Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"` - UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"` - TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"` + Price float64 `form:"price" json:"price" validate:"required,gt=0"` Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"` } diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index 712c6ace..ad1ea8aa 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -190,9 +190,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") } - if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action)) - } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index dd0f99ab..df8a7c98 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -2,16 +2,22 @@ package repository import ( "context" + "fmt" + "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type MarketingRepository interface { repository.BaseRepository[entity.Marketing] IdExists(ctx context.Context, id uint) (bool, error) GetNextSequence(ctx context.Context) (uint, error) + NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) } type MarketingRepositoryImpl struct { @@ -35,3 +41,82 @@ func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, er } return maxID + 1, nil } + +func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) { + return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding) +} + +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 +} + +func (r *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) { + var count int64 + if err := db.WithContext(ctx). + Model(&entity.Marketing{}). + Where(fmt.Sprintf("%s = ?", column), value). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (r *MarketingRepositoryImpl) 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.Marketing{}). + Where(fmt.Sprintf("%s LIKE ?", 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) + +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index d750c4a4..05ffe8ec 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -109,11 +109,10 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context()) + soNumber, err := s.MarketingRepo.NextSoNumber(context.Background(), nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") } - soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -321,21 +320,24 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } if latestApproval != nil { - actorID := uint(1) // todo: ambil dari auth context - action := entity.ApprovalActionUpdated - _, err := approvalSvcTx.CreateApproval( - c.Context(), - utils.ApprovalWorkflowMarketing, - id, - approvalutils.ApprovalStep(latestApproval.StepNumber), - &action, - actorID, - nil) - if err != nil { - if !errors.Is(err, gorm.ErrDuplicatedKey) { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") + if *latestApproval.Action != entity.ApprovalActionUpdated { + actorID := uint(1) // todo: ambil dari auth context + action := entity.ApprovalActionUpdated + _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + id, + utils.MarketingStepPengajuan, + &action, + actorID, + nil) + if err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") + } } } + } return nil diff --git a/internal/modules/production/project_flocks/repositories/project_budget.repository.go b/internal/modules/production/project_flocks/repositories/project_budget.repository.go new file mode 100644 index 00000000..943a22b3 --- /dev/null +++ b/internal/modules/production/project_flocks/repositories/project_budget.repository.go @@ -0,0 +1,23 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ProjectBudgetRepository interface { + repository.BaseRepository[entity.ProjectBudget] +} + +type ProjectBudgetRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectBudget] + db *gorm.DB +} + +func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository { + return &ProjectBudgetRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectBudget](db), + db: db, + } +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 98381df6..e9d0d60d 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -243,6 +243,9 @@ const ( MarketingStepPengajuan approvalutils.ApprovalStep = 1 MarketingStepSalesOrder approvalutils.ApprovalStep = 2 MarketingDeliveryOrder approvalutils.ApprovalStep = 3 + + MarketingSoNumberPrefix = "SO-" + MarketingNumberPadding = 5 ) var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{