resolve conflict to sprint 6

This commit is contained in:
giovanni-ce
2025-12-04 12:20:47 +07:00
91 changed files with 3193 additions and 1131 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ package approvals
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"github.com/gofiber/fiber/v2"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -12,8 +12,8 @@ import (
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
_ = u
ctrl := controller.NewApprovalController(s)
route := v1.Group("/approvals")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
}
@@ -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))
+52 -45
View File
@@ -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
}
}
}
@@ -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) {
+2 -2
View File
@@ -1,7 +1,7 @@
package expenses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers"
expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
ctrl := controller.NewExpenseController(s)
route := v1.Group("/expenses")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -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
}
@@ -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"`
}
@@ -5,8 +5,8 @@ import (
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
@@ -78,7 +78,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return nil, err
}
ctx := c.Context()
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
@@ -131,7 +134,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
LoggableId: 0,
Notes: req.Note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: 1, // TODO: should Get from auth middleware
CreatedBy: actorID, // TODO: should Get from auth middleware
}
if transactionType == entity.TransactionTypeIncrease {
afterQuantity += req.Quantity
@@ -27,7 +27,7 @@ type ProductWarehouseRepository interface {
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error)
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint) (uint, error)
}
type ProductWarehouseRepositoryImpl struct {
@@ -199,7 +199,7 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ctx context.Context,
productID uint,
warehouseID uint,
createdBy uint64,
createdBy uint,
) (uint, error) {
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil {
@@ -3,15 +3,15 @@ package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -127,6 +127,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
}
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
deliveryQtyMap := make(map[uint]float64)
@@ -174,7 +178,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Reason: req.TransferReason,
TransferDate: transferDate,
MovementNumber: movementNumber,
CreatedBy: 1, //todo: get from token
CreatedBy: uint64(actorID),
}
// Save the transfer entity to the database
@@ -280,7 +284,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
LoggableType: entity.LogTypeTransfer,
LoggableId: uint(entityTransfer.Id),
ProductWarehouseId: sourcePW.Id,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
@@ -329,7 +333,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
LoggableId: uint(entityTransfer.Id),
Notes: "",
ProductWarehouseId: destPW.Id,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log increase: %+v", err)
@@ -8,6 +8,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
@@ -175,6 +176,11 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil)
@@ -190,9 +196,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 {
@@ -256,7 +259,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
}
actorID := uint(1) // TODO: ambil dari auth context
approvalAction := entity.ApprovalActionApproved
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
@@ -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)
}
@@ -9,6 +9,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
@@ -90,6 +91,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
); err != nil {
@@ -109,11 +115,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 {
@@ -129,7 +134,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
SoDate: soDate,
SalesPersonId: req.SalesPersonId,
Notes: req.Notes,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders")
@@ -143,7 +148,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
}
}
actorID := uint(1) // TODO: ambil dari auth context
approvalAction := entity.ApprovalActionCreated
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
@@ -180,6 +184,11 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
@@ -321,7 +330,6 @@ 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(),
@@ -405,6 +413,11 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
var action entity.ApprovalAction
@@ -448,7 +461,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
}
}
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
@@ -479,7 +492,6 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
nextStep = approvalutils.ApprovalStep(currentStep)
}
actorID := uint(1) // todo ambil dari auth context
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowMarketing,
@@ -5,6 +5,7 @@ import (
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -87,10 +88,14 @@ func (s *areaService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.A
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", req.Name))
}
//TODO: created by dummy
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.Area{
Name: req.Name,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,11 +1,11 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
}
type Query struct {
@@ -1,16 +1,16 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Alias string `json:"alias" validate:"required_strict"`
Owner *string `json:"owner,omitempty" validate:"omitempty"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Alias string `json:"alias" validate:"required_strict,max=5"`
Owner *string `json:"owner,omitempty" validate:"omitempty,max=50"`
AccountNumber string `json:"account_number" validate:"required_strict,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Alias *string `json:"alias,omitempty" validate:"omitempty"`
Owner *string `json:"owner,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Alias *string `json:"alias,omitempty" validate:"omitempty,max=5"`
Owner *string `json:"owner,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
}
@@ -3,13 +3,13 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -81,6 +81,10 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check customer name: %+v", err)
@@ -100,7 +104,6 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err
}
//TODO: created by dummy
createBody := &entity.Customer{
Name: req.Name,
PicId: req.PicId,
@@ -109,7 +112,7 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
Phone: req.Phone,
Email: req.Email,
AccountNumber: req.AccountNumber,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,23 +1,23 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
Type string `json:"type" validate:"required_strict"`
Type string `json:"type" validate:"required_strict,max=50"`
Address string `json:"address" validate:"required_strict"`
Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email"`
AccountNumber string `json:"account_number" validate:"required_strict"`
Email string `json:"email" validate:"required_strict,email,max=50"`
AccountNumber string `json:"account_number" validate:"required_strict,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
Type *string `json:"type,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty,max=50"`
Address *string `json:"address,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty"`
Email *string `json:"email,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty,max=20"`
Email *string `json:"email,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
}
type Query struct {
+2 -2
View File
@@ -1,7 +1,7 @@
package kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs")
// route.Use(m.Auth(u))
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -3,13 +3,13 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -130,14 +130,18 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
}
//TODO: created by dummy
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.Kandang{
Name: req.Name,
LocationId: req.LocationId,
Capacity: req.Capacity,
Status: status,
PicId: req.PicId,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,8 +1,8 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Status string `json:"status,omitempty" validate:"omitempty,min=3,max=50"`
Capacity float64 `json:"capacity" validate:"required_strict,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
@@ -10,8 +10,8 @@ type Create struct {
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3,max=50"`
Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -4,15 +4,15 @@ import (
"errors"
"fmt"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
@@ -97,12 +97,16 @@ func (s *locationService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err
}
//TODO: created by dummy
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.Location{
Name: req.Name,
Address: req.Address,
AreaId: req.AreaId,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,13 +1,13 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Address string `json:"address" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Address *string `json:"address,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
}
@@ -1,14 +1,14 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
@@ -1,12 +1,12 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Code string `json:"code" validate:"required_strict,max=10"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Code *string `json:"code,omitempty" validate:"omitempty,max=10"`
}
@@ -12,12 +12,13 @@ import (
// === DTO Structs ===
type ProductRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
}
type ProductListDTO struct {
@@ -55,13 +56,20 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
uomRef = &mapped
}
var categoryRef *productCategoryDTO.ProductCategoryRelationDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
categoryRef = &mapped
}
return ProductRelationDTO{
Id: e.Id,
Name: e.Name,
ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice,
Flags: &flags,
Uom: uomRef,
Id: e.Id,
Name: e.Name,
ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice,
Flags: &flags,
Uom: uomRef,
ProductCategory: categoryRef,
}
}
@@ -1,9 +1,9 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Brand string `json:"brand" validate:"required_strict,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
Sku *string `json:"sku,omitempty" validate:"omitempty,max=100"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"`
+2 -2
View File
@@ -1,7 +1,7 @@
package suppliers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ
ctrl := controller.NewSupplierController(s)
route := v1.Group("/suppliers")
// route.Use(m.Auth(u))
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -5,14 +5,14 @@ import (
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
@@ -124,8 +124,10 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
}
alias := strings.TrimSpace(strings.ToUpper(req.Alias))
//TODO: created by dummy
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.Supplier{
Name: req.Name,
Alias: alias,
@@ -139,7 +141,7 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
Npwp: req.Npwp,
AccountNumber: req.AccountNumber,
DueDate: req.DueDate,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,14 +1,14 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Alias string `json:"alias" validate:"required_strict,max=5"`
Pic string `json:"pic" validate:"required_strict"`
Type string `json:"type" validate:"required_strict"`
Category string `json:"category" validate:"required_strict"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty"`
Pic string `json:"pic" validate:"required_strict,max=50"`
Type string `json:"type" validate:"required_strict,max=50"`
Category string `json:"category" validate:"required_strict,max=20"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty,max=50"`
Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email"`
Email string `json:"email" validate:"required_strict,email,max=50"`
Address string `json:"address" validate:"required_strict"`
Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
@@ -16,14 +16,14 @@ type Create struct {
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
Alias *string `json:"alias,omitempty" validate:"omitempty,max=5"`
Pic *string `json:"pic,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty"`
Pic *string `json:"pic,omitempty" validate:"omitempty,max=50"`
Type *string `json:"type,omitempty" validate:"omitempty,max=50"`
Category *string `json:"category,omitempty" validate:"omitempty,max=20"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty,max=50"`
Phone *string `json:"phone,omitempty" validate:"omitempty,max=20"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Email *string `json:"email,omitempty" validate:"omitempty,email,max=50"`
Address *string `json:"address,omitempty" validate:"omitempty"`
Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
@@ -4,14 +4,14 @@ import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
@@ -87,10 +87,13 @@ func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Uo
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", req.Name))
}
//TODO: created by dummy
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.Uom{
Name: req.Name,
CreatedBy: 1,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,11 +1,11 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
}
type Query struct {
@@ -17,7 +17,6 @@ type WarehouseRepository interface {
IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
}
type WarehouseRepositoryImpl struct {
@@ -63,18 +62,6 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
return &warehouse, nil
}
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse
err := r.db.WithContext(ctx).
Preload("Area").
Preload("Location").
First(&warehouse, id).Error
if err != nil {
return nil, err
}
return &warehouse, nil
}
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse
err := r.db.WithContext(ctx).
@@ -3,13 +3,13 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -105,13 +105,15 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
); err != nil {
return nil, err
}
//TODO: created by dummy
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.Warehouse{
Name: req.Name,
Type: typ,
AreaId: req.AreaId,
CreatedBy: 1,
CreatedBy: actorID,
}
if req.LocationId != nil {
createBody.LocationId = req.LocationId
@@ -1,16 +1,16 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Type string `json:"type" validate:"required_strict"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Type string `json:"type" validate:"required_strict,max=50"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Type *string `json:"type,omitempty" validate:"omitempty,max=50"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -8,6 +8,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -125,7 +126,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
actorID := uint(1) // todo nanti ambil dari auth context
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
newChikins := make([]*entity.ProjectChickin, 0)
for _, chickinReq := range req.ChickinRequests {
@@ -356,6 +360,11 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
var action entity.ApprovalAction
@@ -397,14 +406,13 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
step = utils.ProjectFlockKandangStepDisetujui
}
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 {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
actorID := uint(1) // todo nanti ambil dari auth context
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
@@ -1,7 +1,7 @@
package project_flock_kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers"
projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
ctrl := controller.NewProjectFlockKandangController(s)
route := v1.Group("/project-flock-kandangs")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -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,
}
}
@@ -11,7 +11,6 @@ import (
"gorm.io/gorm"
)
type ProjectflockRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
@@ -42,24 +41,23 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
return r.applyQueryFilters(db, params)
return r.applyQueryFilters(r.WithDefaultRelations()(db), params)
})
}
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
Preload("CreatedUser").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
}
}
func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
if params == nil {
return db
@@ -1,7 +1,7 @@
package project_flocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
ctrl := controller.NewProjectflockController(s)
route := v1.Group("/project-flocks")
// route.Use(m.Auth(u))
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -10,8 +10,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
// authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
@@ -85,18 +84,17 @@ func NewProjectflockService(
}
}
func (s projectflockService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, nil, err
}
if params.Page <= 0 {
params.Page = 1
}
if params.Limit <= 0 {
params.Limit = 10
}
offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
@@ -112,7 +110,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
ids[i] = item.Id
}
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.Repository.WithDefaultRelations())
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err)
} else if len(latestMap) > 0 {
@@ -156,7 +154,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr
}
if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations())
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 {
@@ -183,7 +181,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock
}
if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations())
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 {
@@ -221,7 +219,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err
}
actorID, err := actorIDFromContext(c)
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
@@ -344,7 +342,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, err
}
actorID, err := actorIDFromContext(c)
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
@@ -602,7 +600,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
return nil, err
}
actorID, err := actorIDFromContext(c)
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
@@ -847,7 +845,7 @@ func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]
summaries := make([]KandangPeriodSummary, 0, len(rows))
for _, row := range rows {
nextPeriod := 0
nextPeriod := 1
if row.LatestPeriod > 0 {
nextPeriod = row.LatestPeriod + 1
}
@@ -1046,12 +1044,3 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
func actorIDFromContext(_ *fiber.Ctx) (uint, error) {
// user, ok := authmiddleware.AuthenticatedUser(c)
// if !ok || user == nil || user.Id == 0 {
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// return user.Id, nil
return 1, nil
}
@@ -2,6 +2,7 @@ package recordings
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -14,6 +15,7 @@ import (
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -26,6 +28,25 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingStock,
Table: "recording_stocks",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
@@ -41,6 +62,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo,
approvalRepo,
approvalService,
fifoService,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
@@ -25,6 +25,7 @@ type RecordingRepository interface {
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error
@@ -120,6 +121,15 @@ func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]e
return items, nil
}
func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error {
return tx.Model(&entity.RecordingStock{}).
Where("id = ?", stockID).
Updates(map[string]any{
"usage_qty": usageQty,
"pending_qty": pendingQty,
}).Error
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 {
return nil
@@ -4,20 +4,21 @@ import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording"
"math"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -36,6 +37,13 @@ type RecordingService interface {
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
}
type RecordingFIFOIntegrationService interface {
ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
}
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
type recordingService struct {
Log *logrus.Logger
Validate *validator.Validate
@@ -45,6 +53,7 @@ type recordingService struct {
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
}
func NewRecordingService(
@@ -54,6 +63,7 @@ func NewRecordingService(
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate,
) RecordingService {
return &recordingService{
@@ -65,6 +75,20 @@ func NewRecordingService(
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
}
}
func NewRecordingFIFOIntegrationService(
repo repository.RecordingRepository,
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
fifoSvc commonSvc.FifoService,
) RecordingFIFOIntegrationService {
return &recordingService{
Log: utils.Log,
Repository: repo,
ProductWarehouseRepo: productWarehouseRepo,
FifoSvc: fifoSvc,
}
}
@@ -169,7 +193,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
var createdRecording entity.Recording
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId)
@@ -193,7 +220,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
ProjectFlockKandangId: req.ProjectFlockKandangId,
RecordDatetime: recordTime,
Day: &day,
CreatedBy: 1, // TODO: replace with authenticated user
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil {
@@ -219,6 +246,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err
}
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err
}
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to persist depletions: %+v", err)
@@ -231,7 +262,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedStocks, nil, mappedEggs)); err != nil {
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, nil, nil, mappedEggs)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
return err
}
@@ -344,6 +375,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err
}
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
return err
}
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear stocks: %+v", err)
return err
@@ -355,8 +390,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingStocks, mappedStocks, nil, nil)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for stocks: %+v", err)
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err
}
}
@@ -422,7 +456,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
action := entity.ApprovalActionUpdated
actorID := recordingEntity.CreatedBy
if actorID == 0 {
actorID = 1
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
}
var step approvalutils.ApprovalStep
@@ -613,7 +647,10 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
}
ctx := c.Context()
actorID := uint(1) // TODO: replace with authenticated user once auth is integrated
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
@@ -685,7 +722,11 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldStocks, nil, oldEggs, nil)); err != nil {
if err := s.releaseRecordingStocks(ctx, tx, oldStocks); err != nil {
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, nil, nil, oldEggs, nil)); err != nil {
return err
}
@@ -740,6 +781,77 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
return nil
}
func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 || s.FifoSvc == nil {
return nil
}
for _, stock := range stocks {
if stock.Id == 0 {
continue
}
var desired float64
if stock.UsageQty != nil {
desired = *stock.UsageQty
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desired,
AllowPending: true,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.consumeRecordingStocks(ctx, tx, stocks)
}
func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 || s.FifoSvc == nil {
return nil
}
for _, stock := range stocks {
if stock.Id == 0 {
continue
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.releaseRecordingStocks(ctx, tx, stocks)
}
func buildWarehouseDeltas(
oldDepletions, newDepletions []entity.RecordingDepletion,
oldStocks, newStocks []entity.RecordingStock,
@@ -752,12 +864,6 @@ func buildWarehouseDeltas(
for _, item := range newDepletions {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, item.Qty)
}
for _, item := range oldStocks {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, usageQtyValue(item.UsageQty))
}
for _, item := range newStocks {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -usageQtyValue(item.UsageQty))
}
for _, item := range oldEggs {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -float64(item.Qty))
}
@@ -767,13 +873,6 @@ func buildWarehouseDeltas(
return deltas
}
func usageQtyValue(val *float64) float64 {
if val == nil {
return 0
}
return *val
}
func accumulateWarehouseDelta(deltas map[uint]float64, id uint, value float64) {
if id == 0 || value == 0 {
return
@@ -951,7 +1050,7 @@ func (s *recordingService) createRecordingApproval(
return fiber.NewError(fiber.StatusBadRequest, "Recording tidak valid untuk approval")
}
if actorID == 0 {
actorID = 1
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
}
var svc commonSvc.ApprovalService
@@ -1,7 +1,7 @@
package transfer_layings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers"
transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
ctrl := controller.NewTransferLayingController(s)
route := v1.Group("/transfer_layings")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -10,6 +10,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
@@ -154,6 +155,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
@@ -259,7 +265,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate,
PendingUsageQty: &totalSourceQty,
CreatedBy: 1, //todo : harus diambil dari auth
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -592,7 +598,11 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return nil, err
}
actorID := uint(1) // TODO: change from auth context
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
@@ -613,7 +623,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
step = utils.TransferToLayingStepDisetujui
}
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 {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
@@ -23,21 +23,19 @@ func NewPurchaseController(s service.PurchaseService) *PurchaseController {
}
func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
query := &validation.PurchaseQuery{
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search")),
PrNumber: strings.TrimSpace(c.Query("pr_number")),
CreatedFrom: strings.TrimSpace(c.Query("created_from")),
CreatedTo: strings.TrimSpace(c.Query("created_to")),
SupplierID: uint(c.QueryInt("supplier_id", 0)),
AreaID: uint(c.QueryInt("area_id", 0)),
LocationID: uint(c.QueryInt("location_id", 0)),
ProductCategoryID: uint(c.QueryInt("product_category_id", 0)),
}
if supplierID := c.QueryInt("supplier_id", 0); supplierID > 0 {
query.SupplierID = uint(supplierID)
}
if status := strings.TrimSpace(c.Query("status")); status != "" {
query.Status = strings.ToUpper(status)
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
results, total, err := ctrl.service.GetAll(c, query)
@@ -45,24 +43,15 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
return err
}
limit := query.Limit
if limit <= 0 {
limit = 10
}
page := query.Page
if page <= 0 {
page = 1
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PurchaseListItemDTO]{
JSON(response.SuccessWithPaginate[dto.PurchaseListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase fetched successfully",
Meta: response.Meta{
Page: page,
Limit: limit,
TotalPages: int64(math.Ceil(float64(total) / float64(limit))),
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(total) / float64(query.Limit))),
TotalResults: total,
},
Data: dto.ToPurchaseListDTOs(results),
@@ -71,12 +60,13 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
result, err := ctrl.service.GetOne(c, id)
result, err := ctrl.service.GetOne(c, uint(id))
if err != nil {
return err
}
@@ -96,7 +86,7 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.CreateOne(c, req)
if err != nil {
return err
@@ -113,7 +103,7 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
@@ -123,7 +113,7 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
}
result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
result, err := ctrl.service.ApproveStaffPurchase(c, uint(id), req)
if err != nil {
return err
}
@@ -137,10 +127,9 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
})
}
func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
@@ -150,7 +139,7 @@ func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.ApproveManagerPurchase(c, id, req)
result, err := ctrl.service.ApproveManagerPurchase(c, uint(id), req)
if err != nil {
return err
}
@@ -166,7 +155,7 @@ func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
@@ -176,7 +165,7 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.ReceiveProducts(c, id, req)
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
if err != nil {
return err
}
@@ -192,7 +181,7 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
@@ -202,7 +191,7 @@ func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.DeleteItems(c, id, req)
result, err := ctrl.service.DeleteItems(c, uint(id), req)
if err != nil {
return err
}
@@ -218,12 +207,12 @@ func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
func (ctrl *PurchaseController) DeletePurchase(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
if err := ctrl.service.DeletePurchase(c, id); err != nil {
if err := ctrl.service.DeletePurchase(c, uint(id)); err != nil {
return err
}
+111 -84
View File
@@ -10,46 +10,51 @@ import (
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
type PurchaseListItemDTO struct {
Id uint64 `json:"id"`
PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
CreditTerm *int `json:"credit_term"`
DueDate *time.Time `json:"due_date"`
PoDate *time.Time `json:"po_date"`
GrandTotal float64 `json:"grand_total"`
Notes *string `json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval"`
type PurchaseRelationDTO struct {
Id uint `json:"id"`
PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"`
PoDate *time.Time `json:"po_date"`
Notes *string `json:"notes"`
}
type PurchaseListDTO struct {
PurchaseRelationDTO
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
CreditTerm *int `json:"credit_term"`
DueDate *time.Time `json:"due_date"`
GrandTotal float64 `json:"grand_total"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
type PurchaseDetailDTO struct {
Id uint64 `json:"id"`
PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
CreditTerm *int `json:"credit_term"`
DueDate *time.Time `json:"due_date"`
PoDate *time.Time `json:"po_date"`
GrandTotal float64 `json:"grand_total"`
Notes *string `json:"notes"`
Items []PurchaseItemDTO `json:"items"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval"`
PurchaseRelationDTO
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
CreditTerm *int `json:"credit_term"`
DueDate *time.Time `json:"due_date"`
GrandTotal float64 `json:"grand_total"`
Items []PurchaseItemDTO `json:"items"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
type PurchaseItemDTO struct {
Id uint64 `json:"id"`
ProductID uint64 `json:"product_id"`
Id uint `json:"id"`
ProductID uint `json:"product_id"`
Product *productDTO.ProductRelationDTO `json:"product"`
WarehouseID uint64 `json:"warehouse_id"`
WarehouseID uint `json:"warehouse_id"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse"`
ProductWarehouseID *uint64 `json:"product_warehouse_id"`
ProductWarehouseID *uint `json:"product_warehouse_id"`
SubQty float64 `json:"sub_qty"`
TotalQty float64 `json:"total_qty"`
TotalUsed float64 `json:"total_used"`
@@ -61,6 +66,17 @@ type PurchaseItemDTO struct {
VehicleNumber *string `json:"vehicle_number"`
}
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
return PurchaseRelationDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
PoDate: p.PoDate,
Notes: p.Notes,
}
}
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
dto := PurchaseItemDTO{
Id: item.Id,
@@ -77,10 +93,12 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
TravelDocumentPath: item.TravelNumberDocs,
VehicleNumber: item.VehicleNumber,
}
if item.Product != nil && item.Product.Id != 0 {
summary := productDTO.ToProductRelationDTO(*item.Product)
dto.Product = &summary
}
if item.Warehouse != nil && item.Warehouse.Id != 0 {
summary := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse)
if item.Warehouse.Area.Id != 0 {
@@ -93,6 +111,7 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
}
dto.Warehouse = &summary
}
return dto
}
@@ -104,70 +123,78 @@ func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
return result
}
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
dto := PurchaseDetailDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
Items: ToPurchaseItemDTOs(p.Items),
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
var supplier *supplierDTO.SupplierRelationDTO
if p.Supplier.Id != 0 {
mapped := supplierDTO.ToSupplierRelationDTO(p.Supplier)
supplier = &mapped
}
if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
var createdUser *userDTO.UserRelationDTO
if p.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(p.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if p.LatestApproval != nil && p.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
latestApproval = &mapped
}
return PurchaseListDTO{
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
Supplier: supplier,
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
GrandTotal: p.GrandTotal,
CreatedUser: createdUser,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
LatestApproval: latestApproval,
}
return dto
}
func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO {
dto := PurchaseListItemDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
}
return dto
}
func mapSupplier(s entity.Supplier) *supplierDTO.SupplierRelationDTO {
if s.Id == 0 {
return nil
}
summary := supplierDTO.ToSupplierRelationDTO(s)
return &summary
}
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO {
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListDTO {
if len(items) == 0 {
return nil
return make([]PurchaseListDTO, 0)
}
result := make([]PurchaseListItemDTO, len(items))
result := make([]PurchaseListDTO, len(items))
for i, item := range items {
result[i] = ToPurchaseListDTO(item)
}
return result
}
func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalRelationDTO {
if p.LatestApproval == nil || p.LatestApproval.Id == 0 {
return nil
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
var supplier *supplierDTO.SupplierRelationDTO
if p.Supplier.Id != 0 {
mapped := supplierDTO.ToSupplierRelationDTO(p.Supplier)
supplier = &mapped
}
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
return &mapped
}
var createdUser *userDTO.UserRelationDTO
if p.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(p.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if p.LatestApproval != nil && p.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
latestApproval = &mapped
}
return PurchaseDetailDTO{
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
Supplier: supplier,
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
GrandTotal: p.GrandTotal,
Items: ToPurchaseItemDTOs(p.Items),
CreatedUser: createdUser,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
LatestApproval: latestApproval,
}
}
-1
View File
@@ -43,7 +43,6 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo,
supplierRepo,
productWarehouseRepo,
approvalRepo,
approvalService,
expenseBridge,
)
@@ -18,14 +18,11 @@ import (
type PurchaseRepository interface {
repository.BaseRepository[entity.Purchase]
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
WithListRelations() func(*gorm.DB) *gorm.DB
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate, grandTotal float64) error
UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
UpdateGrandTotal(ctx context.Context, purchaseID uint, grandTotal float64) error
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
}
@@ -40,19 +37,10 @@ func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
}
}
type PurchaseListFilter struct {
SupplierID uint
Search string
PrNumber string
CreatedFrom *time.Time
CreatedTo *time.Time
Status *entity.ApprovalAction
CompletedOnly bool
}
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
db := r.DB().WithContext(ctx)
//ambil dari base repository
if err := db.Create(purchase).Error; err != nil {
return err
}
@@ -71,7 +59,7 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
return nil
}
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
if len(items) == 0 {
return nil
}
@@ -86,52 +74,9 @@ func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uin
return r.DB().WithContext(ctx).Create(&items).Error
}
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
var purchase entity.Purchase
err := r.DB().WithContext(ctx).
Scopes(r.withDetailRelations).
First(&purchase, id).Error
if err != nil {
return nil, err
}
return &purchase, nil
}
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withListRelations(db)
return r.applyListFilters(db, filter)
})
}
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withListRelations(db)
}
}
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Supplier").
Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Order("id ASC")
}).
Preload("Items.Product").
Preload("Items.Warehouse").
Preload("Items.Warehouse.Area").
Preload("Items.Warehouse.Location").
Preload("Items.ProductWarehouse")
}
func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDetailRelations(db)
}
}
type PurchasePricingUpdate struct {
ItemID uint64
ProductID *uint64
ItemID uint
ProductID *uint
Price float64
TotalPrice float64
Quantity *float64
@@ -139,7 +84,7 @@ type PurchasePricingUpdate struct {
}
type PurchaseReceivingUpdate struct {
ItemID uint64
ItemID uint
ReceivedDate *time.Time
TravelNumber *string
TravelDocumentPath *string
@@ -152,7 +97,7 @@ type PurchaseReceivingUpdate struct {
func (r *PurchaseRepositoryImpl) UpdatePricing(
ctx context.Context,
purchaseID uint64,
purchaseID uint,
updates []PurchasePricingUpdate,
grandTotal float64,
) error {
@@ -192,7 +137,6 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
Where("id = ?", purchaseID).
Updates(map[string]interface{}{
"grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error; err != nil {
return err
}
@@ -202,7 +146,7 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
ctx context.Context,
purchaseID uint64,
purchaseID uint,
updates []PurchaseReceivingUpdate,
) error {
if len(updates) == 0 {
@@ -259,7 +203,7 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
ctx context.Context,
purchaseID uint64,
purchaseID uint,
grandTotal float64,
) error {
return r.DB().WithContext(ctx).
@@ -271,7 +215,7 @@ func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
}).Error
}
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error {
if len(itemIDs) == 0 {
return errors.New("itemIDs cannot be empty")
}
@@ -361,63 +305,3 @@ func parseNumericSuffix(value, prefix string) (int, bool) {
}
return number, true
}
func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Supplier")
}
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
if filter == nil {
return db
}
if filter.SupplierID > 0 {
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
}
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
like := "%" + search + "%"
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
}
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
}
if filter.CreatedFrom != nil {
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
}
if filter.CreatedTo != nil {
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
}
if filter.CompletedOnly {
step := uint16(utils.PurchaseStepCompleted)
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
} else if filter.Status != nil {
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
}
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
}
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
latestSub := r.DB().
Model(&entity.Approval{}).
Select("approvable_id, MAX(action_at) AS latest_action_at").
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
Group("approvable_id")
db = db.
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
Joins(
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
utils.ApprovalWorkflowPurchase.String(),
).
Where("approvals.action = ?", string(action))
if minStep != nil {
db = db.Where("approvals.step_number >= ?", *minStep)
}
return db
}
+2 -2
View File
@@ -3,7 +3,7 @@ package purchases
import (
"github.com/gofiber/fiber/v2"
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe
ctrl := controller.NewPurchaseController(purchaseService)
route := router.Group("/purchases")
route.Use(middleware.Auth(userService))
route.Use(m.Auth(userService))
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
@@ -9,16 +9,16 @@ import (
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
type PurchaseExpenseBridge interface {
OnItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) error
OnItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
OnItemsReceived(ctx context.Context, purchaseID uint64, updates []ExpenseReceivingPayload) error
OnItemsCreated(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
OnItemsDeleted(ctx context.Context, purchaseID uint, itemIDs []uint) error
OnItemsReceived(ctx context.Context, purchaseID uint, updates []ExpenseReceivingPayload) error
}
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
type ExpenseReceivingPayload struct {
PurchaseItemID uint64
ProductID uint64
WarehouseID uint64
PurchaseItemID uint
ProductID uint
WarehouseID uint
ReceivedQty float64
ReceivedDate *time.Time
}
@@ -30,14 +30,14 @@ func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
return &noopPurchaseExpenseBridge{}
}
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint64, _ []entity.PurchaseItem) error {
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint, _ []entity.PurchaseItem) error {
return nil
}
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint64, _ []uint64) error {
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint, _ []uint) error {
return nil
}
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint64, _ []ExpenseReceivingPayload) error {
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint, _ []ExpenseReceivingPayload) error {
return nil
}
File diff suppressed because it is too large Load Diff
@@ -14,26 +14,28 @@ type CreatePurchaseRequest struct {
}
type StaffPurchaseApprovalItem struct {
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
PurchaseItemID uint `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
// For new items (no purchase_item_id), product_id is required.
ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
ProductID uint `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
WarehouseID uint `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
}
type ApproveStaffPurchaseRequest struct {
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
Items []StaffPurchaseApprovalItem `json:"items,omitempty" validate:"omitempty,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type ApproveManagerPurchaseRequest struct {
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type ReceivePurchaseItemRequest struct {
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"`
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
@@ -43,21 +45,23 @@ type ReceivePurchaseItemRequest struct {
}
type ReceivePurchaseRequest struct {
Items []ReceivePurchaseItemRequest `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
Items []ReceivePurchaseItemRequest `json:"items,omitempty" validate:"omitempty,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type DeletePurchaseItemsRequest struct {
ItemIDs []uint64 `json:"item_ids" validate:"required,min=1,dive,gt=0"`
ItemIDs []uint `json:"item_ids" validate:"required,min=1,dive,gt=0"`
}
type PurchaseQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
PrNumber string `query:"pr_number" validate:"omitempty,max=50"`
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
Status string `query:"status" validate:"omitempty,oneof=CREATED UPDATED APPROVED REJECTED COMPLETED"`
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
AreaID uint `query:"area_id" validate:"omitempty,gt=0"`
LocationID uint `query:"location_id" validate:"omitempty,gt=0"`
ProductCategoryID uint `query:"product_category_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
}